イメージパイプラインの拡張
Android は、多くの画像フォーマット を標準でサポートしていますが、サポートしていないフォーマットも多数存在します (例: GIF、SVG、MP4 など)。
幸いなことに、ImageLoader は、新しいキャッシュレイヤー、新しいデータ型、新しいフェッチ動作、新しい画像エンコーディングを追加したり、ベースの画像読み込み動作を上書きしたりするためのプラグイン可能なコンポーネントをサポートしています。Coil のイメージパイプラインは、以下の順序で実行される5つの主要な部分で構成されています: Interceptors、Mappers、Keyers、Fetchers、そして Decoders。
カスタムコンポーネントは、ComponentRegistry を介して ImageLoader を構築する際に追加する必要があります:
val imageLoader = ImageLoader.Builder(context)
.components {
add(CustomCacheInterceptor())
add(ItemMapper())
add(HttpUrlKeyer())
add(CronetFetcher.Factory())
add(GifDecoder.Factory())
}
.build()Interceptors
Interceptors は、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)
}
}Interceptors は、ImageLoader のイメージパイプラインをカスタムロジックでラップできる高度な機能です。その設計は、OkHttp の Interceptor インターフェース に大きく基づいています。
詳細については、Interceptor を参照してください。
Mappers
Mappers を使用すると、カスタムデータ型へのサポートを追加できます。例えば、サーバーからこのようなモデルを取得するとします:
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
Keyers はデータをキャッシュキーの一部に変換します。この値は、このリクエストの出力が MemoryCache に書き込まれる際に MemoryCache.Key.key として使用されます。
詳細については、Keyers を参照してください。
Fetchers
Fetchers は、データ (例: URL、URI、File など) を ImageSource または Image のいずれかに変換します。これらは通常、入力データを Decoder によって消費できる形式に変換します。このインターフェースを使用して、カスタムフェッチメカニズム (例: Cronet、カスタムURIスキームなど) のサポートを追加します。
詳細については、Fetcher を参照してください。
::: Note カスタムデータ型を使用する Fetcher を追加する場合、それを使用するリクエストの結果がメモリキャッシュ可能であることを保証するために、カスタム Keyer も提供する必要があります。例えば、Fetcher.Factory<MyDataType> は Keyer<MyDataType> を追加する必要があります。
:::
Decoders
Decoders は ImageSource を読み取り、Image を返します。このインターフェースを使用して、カスタムファイルフォーマット (例: GIF、SVG、TIFF など) のサポートを追加します。
詳細については、Decoder を参照してください。
カスタム ImageLoader と ImageRequest のプロパティ
Coil は、ImageRequest と ImageLoader の Extras を介してカスタムデータを付加することをサポートしています。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)
// NOTE: Extras.Key instances should be declared statically as they're compared with instance equality.
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()
// Read the image URL.
val imageUrl: String = readImageUrl(response.body)
// This will delegate to the internal network fetcher.
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 を登録し、PartialUrl を model/data として渡すだけです:
AsyncImage(
model = PartialUrl("https://example.com/image.jpg"),
contentDescription = null,
)このパターンは、Mapper、Keyer、Decoder にも同様に適用できます。
