イメージパイプラインの拡張
Androidは多くの画像形式を標準でサポートしていますが、サポートしていない形式(GIF、SVG、MP4など)も多く存在します。
幸いなことに、ImageLoaderは、新しいキャッシュレイヤー、新しいデータ型、新しいフェッチ動作、新しい画像エンコーディングを追加したり、ベースの画像読み込み動作を上書きしたりするためのプラグイン可能なコンポーネントをサポートしています。Coilのイメージパイプラインは、主に5つのパーツで構成されており、Interceptors、Mappers、Keyers、Fetchers、Decodersの順に実行されます。
カスタムコンポーネントは、ImageLoaderを構築する際に、そのComponentRegistryを通じて追加する必要があります。
val imageLoader = ImageLoader.Builder(context)
.components {
add(CustomCacheInterceptor())
add(ItemMapper())
add(HttpUrlKeyer())
add(CronetFetcher.Factory())
add(GifDecoder.Factory())
}
.build()Interceptors
Interceptor(インターセプター)を使用すると、ImageLoaderの画像エンジンのリクエストを監視、変換、ショートサーキット(処理の打ち切り)、またはリトライできます。たとえば、次のようにカスタムキャッシュレイヤーを追加できます。
class CustomCacheInterceptor(
private val context: Context,
private val cache: LruCache<String, Image>,
) : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val value = cache.get(chain.request.data.toString())
if (value != null) {
return SuccessResult(
image = value.bitmap.toImage(),
request = chain.request,
dataSource = DataSource.MEMORY_CACHE,
)
}
return chain.proceed(chain.request)
}
}Interceptorは、ImageLoaderのイメージパイプラインをカスタムロジックでラップできる高度な機能です。その設計は、OkHttpのInterceptorインターフェースに強く基づいています。
詳細については、Interceptorを参照してください。
Mappers
Mapper(マッパー)を使用すると、カスタムデータ型のサポートを追加できます。たとえば、サーバーから次のようなモデルを取得するとします。
data class Item(
val id: Int,
val imageUrl: String,
val price: Int,
val weight: Double
)これをURLにマッピングするカスタムマッパーを作成できます。このURLは、パイプラインの後半で処理されます。
class ItemMapper : Mapper<Item, String> {
override fun map(data: Item, options: Options) = data.imageUrl
}ImageLoaderの構築時に登録(上記参照)すると、Itemを安全にロードできるようになります。
val request = ImageRequest.Builder(context)
.data(item)
.target(imageView)
.build()
imageLoader.enqueue(request)詳細については、Mapperを参照してください。
Keyers
Keyer(キーヤー)は、データをキャッシュキーの一部に変換します。この値は、リクエストの出力がMemoryCacheに書き込まれる際にMemoryCache.Key.keyとして使用されます。
詳細については、Keyersを参照してください。
Fetchers
Fetcher(フェッチャー)は、データ(URL、URI、Fileなど)をImageSourceまたはImageに変換します。通常、入力データをDecoderで処理可能な形式に変換します。このインターフェースを使用して、カスタムフェッチメカニズム(Cronet、カスタムURIスキームなど)のサポートを追加します。
詳細については、Fetcherを参照してください。
::: Note カスタムデータ型を使用するFetcherを追加する場合は、その結果をメモリキャッシュ可能にするために、カスタムのKeyerも提供する必要があります。たとえば、Fetcher.Factory<MyDataType>にはKeyer<MyDataType>を追加する必要があります。
:::
Decoders
Decoder(デコーダー)は、ImageSourceを読み取り、Imageを返します。このインターフェースを使用して、カスタムファイル形式(GIF、SVG、TIFFなど)のサポートを追加します。
詳細については、Decoderを参照してください。
Custom ImageLoader and ImageRequest properties
Coilは、Extrasを通じてImageRequestやImageLoaderにカスタムデータを添付することをサポートしています。Extrasは、Extras.Keyを介して参照される拡張プロパティのマップです。
たとえば、各ImageRequestにカスタムタイムアウトをサポートしたい場合、次のようにカスタム拡張関数を追加できます。
fun ImageRequest.Builder.timeout(timeout: Duration) = apply {
extras[timeoutKey] = timeout
}
fun ImageLoader.Builder.timeout(timeout: Duration) = apply {
extras[timeoutKey] = timeout
}
val ImageRequest.timeout: Duration
get() = getExtra(timeoutKey)
val Options.timeout: Duration
get() = getExtra(timeoutKey)
// 注: Extras.Key インスタンスはインスタンスの同一性で比較されるため、静的に宣言する必要があります。
private val timeoutKey = Extras.Key(default = Duration.INFINITE)その後、ImageLoaderに登録するカスタムInterceptor内でこのプロパティを読み取ることができます。
class TimeoutInterceptor : Interceptor {
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
val timeout = chain.request.timeout
if (timeout.isFinite()) {
return withTimeout(timeout) {
chain.proceed(chain.request)
}
} else {
return chain.proceed(chain.request)
}
}
}最後に、ImageRequestを作成するときにプロパティを設定できます。
AsyncImage(
model = ImageRequest.Builder(PlatformContext.current)
.data("https://example.com/image.jpg")
.timeout(10.seconds)
.build(),
contentDescription = null,
)さらに:
- 定義した
ImageLoader.Builder.timeout拡張関数を介して、デフォルトのタイムアウト値を設定できます。 - 定義した
Options.timeout拡張関数を介して、Mapper、Fetcher、Decoder内でタイムアウトを読み取ることができます。
Coil自体もこのパターンを使用しており、coil-gifやその他の拡張ライブラリにおいて、GIF用のカスタムリクエストプロパティなどをサポートしています。
Chaining components
Coilのイメージローダーコンポーネントの便利な特性は、内部的にチェイン(連結)できることです。たとえば、読み込む画像URLを取得するためにネットワークリクエストを実行する必要があるとします。
まず、フェッチャーのみが処理するカスタムデータ型を作成します。
data class PartialUrl(
val baseUrl: String,
)次に、画像URLを取得し、内部のネットワークフェッチャーに委譲するカスタムFetcherを作成します。
class PartialUrlFetcher(
private val callFactory: Call.Factory,
private val partialUrl: PartialUrl,
private val options: Options,
private val imageLoader: ImageLoader,
) : Fetcher {
override suspend fun fetch(): FetchResult? {
val request = Request.Builder()
.url(partialUrl.baseUrl)
.build()
val response = callFactory.newCall(request).await()
// 画像URLを読み取る
val imageUrl: String = readImageUrl(response.body)
// 内部のネットワークフェッチャーに委譲する
val data = imageLoader.components.map(imageUrl, options)
val output = imageLoader.components.newFetcher(data, options, imageLoader)
val (fetcher) = checkNotNull(output) { "no supported fetcher" }
return fetcher.fetch()
}
class Factory(
private val callFactory: Call.Factory = OkHttpClient(),
) : Fetcher.Factory<PartialUrl> {
override fun create(data: PartialUrl, options: Options, imageLoader: ImageLoader): Fetcher {
return PartialUrlFetcher(callFactory, data, options, imageLoader)
}
}
}最後に、ComponentRegistryにFetcherを登録し、model/dataとしてPartialUrlを渡すだけです。
AsyncImage(
model = PartialUrl("https://example.com/image.jpg"),
contentDescription = null,
)このパターンは、Mapper、Keyer、Decoderにも同様に適用できます。
