Skip to content

イメージパイプラインの拡張

Android は、多くの画像フォーマット を標準でサポートしていますが、サポートしていないフォーマットも多数存在します (例: GIF、SVG、MP4 など)。

幸いなことに、ImageLoader は、新しいキャッシュレイヤー、新しいデータ型、新しいフェッチ動作、新しい画像エンコーディングを追加したり、ベースの画像読み込み動作を上書きしたりするためのプラグイン可能なコンポーネントをサポートしています。Coil のイメージパイプラインは、以下の順序で実行される5つの主要な部分で構成されています: InterceptorsMappersKeyersFetchers、そして Decoders

カスタムコンポーネントは、ComponentRegistry を介して ImageLoader を構築する際に追加する必要があります:

kotlin
val imageLoader = ImageLoader.Builder(context)
    .components {
        add(CustomCacheInterceptor())
        add(ItemMapper())
        add(HttpUrlKeyer())
        add(CronetFetcher.Factory())
        add(GifDecoder.Factory())
    }
    .build()

Interceptors

Interceptors は、ImageLoader のイメージエンジンへのリクエストを監視、変換、ショートサーキット、または再試行することを可能にします。例えば、次のようにカスタムキャッシュレイヤーを追加できます:

kotlin
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 を使用すると、カスタムデータ型へのサポートを追加できます。例えば、サーバーからこのようなモデルを取得するとします:

kotlin
data class Item(
    val id: Int,
    val imageUrl: String,
    val price: Int,
    val weight: Double
)

カスタムマッパーを作成して、それをURLにマッピングできます。このURLはパイプラインの後半で処理されます:

kotlin
class ItemMapper : Mapper<Item, String> {
    override fun map(data: Item, options: Options) = data.imageUrl
}

(上記参照) ImageLoader を構築する際にそれを登録した後、Item を安全にロードできます:

kotlin
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 を参照してください。

カスタム ImageLoaderImageRequest のプロパティ

Coil は、ImageRequestImageLoaderExtras を介してカスタムデータを付加することをサポートしています。Extras は、Extras.Key を介して参照される追加プロパティのマップです。

例えば、各 ImageRequest にカスタムタイムアウトをサポートしたいとします。そのためのカスタム拡張関数を次のように追加できます:

kotlin
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 内でそのプロパティを読み取ることができます:

kotlin
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 を作成する際にそのプロパティを設定できます:

kotlin
AsyncImage(
    model = ImageRequest.Builder(PlatformContext.current)
        .data("https://example.com/image.jpg")
        .timeout(10.seconds)
        .build(),
    contentDescription = null,
)

加えて:

  • 定義した ImageLoader.Builder.timeout 拡張関数を介してデフォルトのタイムアウト値を設定できます。
  • 定義した Options.timeout 拡張関数を介して、MapperFetcherDecoder 内でタイムアウトを読み取ることができます。

Coil 自体もこのパターンを使用しています 。これは、coil-gif における GIF のカスタムリクエストプロパティや、他の拡張ライブラリをサポートするためです。

Chaining components

Coil のイメージローダーコンポーネントの便利な特性は、それらが内部的にチェーンできることです。例えば、ロードされる画像URLを取得するためにネットワークリクエストを実行する必要があるとします。

まず、私たちのフェッチャーのみが処理するカスタムデータ型を作成しましょう:

kotlin
data class PartialUrl(
    val baseUrl: String,
)

次に、画像URLを取得し、内部ネットワークフェッチャーに委譲するカスタム Fetcher を作成しましょう:

kotlin
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)
        }
    }
}

最後に、ComponentRegistryFetcher を登録し、PartialUrlmodel/data として渡すだけです:

kotlin
AsyncImage(
    model = PartialUrl("https://example.com/image.jpg"),
    contentDescription = null,
)

このパターンは、MapperKeyerDecoder にも同様に適用できます。