Customizing Image Loading

The Compose SDK loads images, GIFs, and video thumbnails through Coil 3. The loader is exposed on ChatTheme via the imageLoaderFactory property — defaulting to StreamCoilImageLoaderFactory.defaultFactory() — so you can customize image loading globally without re-implementing any rendering code.

This page describes what the default factory provides and how to safely extend or replace it.

StreamCoilImageLoaderFactory

StreamCoilImageLoaderFactory.defaultFactory() provides the default factory that creates new Coil ImageLoader instances.

You can find out more about it by reading the class documentation.

You can customize how images are loaded by passing your own implementation of StreamCoilImageLoaderFactory to ChatTheme, or by using the default factory to provide an ImageLoader and calling loader.newBuilder() to expand or override its behavior.

Default Coil Components

The built-in StreamImageLoaderFactory registers:

  • OkHttpNetworkFetcherFactory — fetches image bytes over HTTP/HTTPS. Registered explicitly in code so it is not affected by R8 full-mode stripping the Coil 3 ServiceLoader registration.
  • AnimatedImageDecoder (API 28+) / GifDecoder — decodes GIF and animated WebP attachments, including Giphy.
  • VideoFrameDecoder — decodes the first frame of video attachments to display video thumbnails.

If you provide a fully custom StreamCoilImageLoaderFactory without delegating to StreamImageLoaderFactory, you must re-register these components yourself, otherwise GIFs render as static frames, video thumbnails go missing, and HTTP fetching can fail under R8 full mode.

Customizing the Component Registry

ImageLoader.Builder.components { … } replaces the existing component registry on the builder — it is not additive. Because the StreamImageLoaderFactory builder lambda runs after the SDK populates its own components, any components { … } block you supply overwrites the SDK defaults.

If you need a custom Coil component (decoder, mapper, or network fetcher), re-register every component the SDK provides alongside your additions:

val factory = StreamCoilImageLoaderFactory { context ->
    StreamImageLoaderFactory {
        components {
            // SDK defaults — must be re-registered when you supply your own components block.
            add(OkHttpNetworkFetcherFactory())
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                add(AnimatedImageDecoder.Factory(enforceMinimumFrameDelay = true))
            } else {
                add(GifDecoder.Factory(enforceMinimumFrameDelay = true))
            }
            add(VideoFrameDecoder.Factory())

            // Your additions:
            add(MyCustomDecoder.Factory())
        }
    }.newImageLoader(context)
}

ChatTheme(imageLoaderFactory = factory) {
    // Your UI content
}

Need auth headers or signed URLs on image requests? Use the CDN interface instead of customizing Coil. CDN.imageRequest applies to every image URL the SDK loads and avoids the need to replace the loader's component registry.

Troubleshooting: blank or placeholder images

Apps on the default loader are not affected — StreamImageLoaderFactory registers the OkHttp fetcher in code. Manual component registration only matters if you provide a fully custom ImageLoader.

Symptoms are typically release-build-only with R8 enabled, because Coil 3 auto-registers the fetcher through ServiceLoader, which R8 full-mode can strip. The snippet under Customizing the Component Registry is the canonical component list — register the OkHttp fetcher there explicitly to bypass auto-registration.