Skip to content

Extending the Image Pipeline

Android supports many image formats out of the box, however there are also plenty of formats it does not (e.g. GIF, SVG, MP4, etc.)

Fortunately, ImageLoaders support pluggable components to add new cache layers, new data types, new fetching behavior, new image encodings, or otherwise overwrite the base image loading behavior. Coil's image pipeline consists of five main parts that are executed in the following order: Interceptors, Mappers, Keyers, Fetchers, and Decoders.

Custom components must be added to the ImageLoader when constructing it through its ComponentRegistry:

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

Interceptors

Interceptors allow you to observe, transform, short circuit, or retry requests to an ImageLoader's image engine. For example, you can add a custom cache layer like so:

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 are an advanced feature that let you wrap an ImageLoader's image pipeline with custom logic. Their design is heavily based on OkHttp's Interceptor interface.

See Interceptor for more information.

Mappers

Mappers allow you to add support for custom data types. For instance, say we get this model from our server:

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

We could write a custom mapper to map it to its URL, which will be handled later in the pipeline:

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

After registering it when building our ImageLoader (see above), we can safely load an Item:

val request = ImageRequest.Builder(context)
    .data(item)
    .target(imageView)
    .build()
imageLoader.enqueue(request)

See Mapper for more information.

Keyers

Keyers convert data into a portion of a cache key. This value is used as MemoryCache.Key.key when/if this request's output is written to the MemoryCache.

See Keyers for more information.

Fetchers

Fetchers translate data (e.g. URL, URI, File, etc.) into either an ImageSource or an Image. They typically convert the input data into a format that can then be consumed by a Decoder. Use this interface to add support for custom fetching mechanisms (e.g. Cronet, custom URI schemes, etc.)

See Fetcher for more information.

Note

If you add a Fetcher that uses a custom data type, you need to also need to provide a custom Keyer to ensure results from requests that use it are memory cacheable. For example, Fetcher.Factory<MyDataType> will need to add a Keyer<MyDataType.

Decoders

Decoders read an ImageSource and return an Image. Use this interface to add support for custom file formats (e.g. GIF, SVG, TIFF, etc.).

See Decoder for more information.

Custom ImageLoader and ImageRequest properties

Coil supports attaching custom data to ImageRequests and ImageLoaders through their Extras. Extras are a map of extra properties that are referenced via an Extras.Key.

For example, say we want to support a custom timeout for each ImageRequest. We could add custom extension functions for it like so:

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)

Then we can read the property inside a custom Interceptor that we'll register in our ImageLoader:

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

Finally, we can set the property when creating our ImageRequest:

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

Additionally:

  • We can set a default timeout value via the ImageLoader.Builder.timeout extension function we defined.
  • We can read the timeout inside Mappers, Fetchers, and Decoders via the Options.timeout extension function we defined.

Coil uses this pattern itself to support custom request properties for GIFs in coil-gif as well as other extension libraries.

Chaining components

A useful property of Coil's image loader components is that they can be chained internally. For example, say you need to perform a network request to get the image URL that will be loaded.

First, let's create a custom data type that only our fetcher will handle:

data class PartialUrl(
    val baseUrl: String,
)

Then let's create our custom Fetcher that will get the image URL and delegate to the internal network 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)
        }
    }
}

Finally all we have to do is register the Fetcher in our ComponentRegistry and pass a PartialUrl as our model/data:

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

This pattern can similarly be applied to Mappers, Keyers, and Decoders.