Skip to content

Image Loaders

ImageLoaders are service objects that execute ImageRequests. They handle caching, data fetching, image decoding, request management, bitmap pooling, memory management, and more. New instances can be created and configured using a builder:

val imageLoader = ImageLoader.Builder(context)
    .availableMemoryPercentage(0.25)
    .crossfade(true)
    .build()

Coil performs best when you create a single ImageLoader and share it throughout your app. This is because each ImageLoader has its own memory cache, bitmap pool, and network observer.

It's recommended, though not required, to call shutdown when you've finished using an image loader. This preemptively frees its memory and cleans up any observers. If you only create and use one ImageLoader, you do not need to shut it down as it will be freed when your app is killed.

Caching

Each ImageLoader keeps a memory cache of recently decoded Bitmaps as well as a reusable pool of Bitmaps to decode into.

ImageLoaders rely on an OkHttpClient to handle disk caching. By default, every ImageLoader is already set up for disk caching and will set a max cache size of between 10-250MB depending on the remaining space on the user's device.

However, if you set a custom OkHttpClient, you'll need to add the disk cache yourself. To get a Cache instance that's optimized for Coil, you can use CoilUtils.createDefaultCache. Optionally, you can create your own Cache instance with a different size + location. Here's an example:

val imageLoader = ImageLoader.Builder(context)
    .okHttpClient {
        OkHttpClient.Builder()
            .cache(CoilUtils.createDefaultCache(context))
            .build()
    }
    .build()

Singleton vs. Dependency Injection

Coil performs best when you create a single ImageLoader and share it throughout your app. This is because each ImageLoader has its own memory cache and bitmap pool.

If you use a dependency injector like Dagger, then you should create a single ImageLoader instance and inject it throughout your app.

However, if you'd prefer a singleton the io.coil-kt:coil artifact provides a singleton ImageLoader instance that can be accessed using the Context.imageLoader extension function. Read here for how to initialize the singleton ImageLoader instance.

Note

Use the io.coil-kt:coil-base artifact if you are using dependency injection.

Testing

ImageLoader is an interface, which you can replace with a fake implementation.

For instance, you could inject a fake ImageLoader implementation which always returns the same Drawable synchronously:

val fakeImageLoader = object : ImageLoader {

    private val disposable = object : Disposable {
        override val isDisposed get() = true
        override fun dispose() {}
        override suspend fun await() {}
    }

    override val defaults = DefaultRequestOptions()

    // Optionally, you can add a custom fake memory cache implementation.
    override val memoryCache get() = throw UnsupportedOperationException()

    override val bitmapPool = BitmapPool(0)

    override fun enqueue(request: ImageRequest): Disposable {
        // Always call onStart before onSuccess.
        request.target?.onStart(placeholder = ColorDrawable(Color.BLACK))
        request.target?.onSuccess(result = ColorDrawable(Color.BLACK))
        return disposable
    }

    override suspend fun execute(request: ImageRequest): ImageResult {
        return SuccessResult(
            drawable = ColorDrawable(Color.BLACK),
            request = request,
            metadata = ImageResult.Metadata(
                memoryCacheKey = MemoryCache.Key(""),
                isSampled = false,
                dataSource = DataSource.MEMORY_CACHE,
                isPlaceholderMemoryCacheKeyPresent = false
            )
        )
    }

    override fun shutdown() {}

    override fun newBuilder() = ImageLoader.Builder(context)
}

This is perfect for screenshot and instrumentation tests where you want consistent rendering behavior.