coil Documentation

Image loading for Android and Compose Multiplatform.

RAW Doc

Technical Documentation: coil-kt/coil

ℹ️ Provenance: Multi-Tier Ingestion Β· CodeWiki Reference

Overview (README.md)

An image loading library for Android and Compose Multiplatform. Coil is:

  • Fast: Coil performs a number of optimizations including memory and disk caching, downsampling the image, automatically pausing/cancelling requests, and more.
  • Lightweight: Coil only depends on Kotlin, Coroutines, and Okio and works seamlessly with Google's R8 code shrinker.
  • Easy to use: Coil's API leverages Kotlin's language features for simplicity and minimal boilerplate.
  • Modern: Coil is Kotlin-first and interoperates with modern libraries including Compose, Coroutines, Okio, OkHttp, and Ktor.

Coil is an acronym for: Coroutine Image Loader.

Translations: ζ—₯本θͺž, ν•œκ΅­μ–΄, Русский, Svenska, TΓΌrkΓ§e, δΈ­ζ–‡, پارسی, O'zbekcha

Quick Start

Import the Compose library and a networking library:

kotlin
implementation("io.coil-kt.coil3:coil-compose:3.6.3")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.6.3")

To load an image, use the AsyncImage composable:

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

Check out Coil's full documentation here.

License

text
Copyright 2026 Coil Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Code Of Conduct (`docs/code_of_conduct.md`)

Code of Conduct

Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.

Our Standards

Examples of behavior that contributes to creating a positive environment
include:

  • Using welcoming and inclusive language
  • Being respectful of differing viewpoints and experiences
  • Gracefully accepting constructive criticism
  • Focusing on what is best for the community
  • Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

  • The use of sexualized language or imagery and unwelcome sexual attention or
    advances
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others' private information, such as a physical or electronic
    address, without explicit permission
  • Other conduct which could reasonably be considered inappropriate in a
    professional setting

Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

Scope

This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at colin at colinwhite.me. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq


Faq (`docs/faq.md`)

FAQ

Have a question that isn't part of the FAQ? Check StackOverflow with the tag #coil or search Github discussions.

Can Coil be used with Java projects or mixed Kotlin/Java projects?

Yes! Read here.

How do I preload an image?

Launch an image request with no target:

kotlin
val request = ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    .build()
imageLoader.enqueue(request)

That will preload the image and save it to the disk and memory caches.

If you only want to preload to the disk cache you can skip decoding and saving to the memory cache like so:

kotlin
val request = ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    // Disables writing to the memory cache.
    .memoryCachePolicy(CachePolicy.DISABLED)
    // Skips the decode step so we don't waste time/memory decoding the image into memory.
    .decoderFactory(BlackholeDecoder.Factory())
    .build()
imageLoader.enqueue(request)

How do I enable logging?

Set logger(DebugLogger()) when constructing your ImageLoader.

!!! Note
DebugLogger should only be used in debug builds.

How do I target Java 8 or Java 11?

Coil requires Java 8 bytecode. This is enabled by default on the Android Gradle Plugin 4.2.0 and later and the Kotlin Gradle Plugin 1.5.0 and later. If you're using older versions of those plugins add the following to your Gradle build script:

kotlin
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

As of Coil 3.2.0, Java 11 bytecode is required for coil-compose and coil-compose-core:

kotlin
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
}

Why do I get a Skiko version warning with Compose Multiplatform?

Compose Multiplatform will print a warning like this if Coil's Skiko dependency is older than Compose Multiplatform's:

text
w: Skiko dependencies' versions are incompatible.

Skiko does not guarantee binary compatibility between milestone versions because it wraps Skia, whose milestone versions can introduce breaking changes. This warning should not be assumed safe to ignore. Coil releases track Compose Multiplatform stable releases and their Skiko versions, so if you encounter this warning, first update Coil to the latest version.

NOTE: Coil doesn't release new versions only to match the Skiko versions used by alpha and beta Compose Multiplatform releases. However, if there is a binary compatibility issue that requires Coil to depend on a newer Skiko version, Coil will publish an alpha release with the updated dependency.

If you've updated Coil to the latest version and the warning is still present you can suppress the warning like so:

Compose Multiplatform >= 1.13.0

Add the following to the build.gradle.kts file of each module that applies the Compose Multiplatform plugin:

kotlin
compose {
    dependencyCompatibility {
        exclude("io.coil-kt.coil3")
    }
}

This disables the compatibility check only for Coil.

Older Compose Multiplatform versions:

Add the following to your gradle.properties file:

properties
org.jetbrains.compose.library.compatibility.check.disable=true

This disables all Compose Multiplatform library compatibility checks for all libraries, including Coil.

How do I get development snapshots?

Add the snapshots repository to your list of repositories:

Gradle (.gradle):

groovy
allprojects {
    repositories {
        maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
    }
}

Gradle Kotlin DSL (.gradle.kts):

kotlin
allprojects {
    repositories {
        maven("https://central.sonatype.com/repository/maven-snapshots/")
    }
}

Then depend on the same artifacts with the latest snapshot version.

!!! Note
Snapshots are deployed for each new commit on main that passes CI. They can potentially contain breaking changes or may be unstable. Use at your own risk.

How to I use Proguard with Coil?

To use Proguard with Coil, add these Proguard rules to your config.

You may also need to add custom rules for Ktor, OkHttp, and Coroutines.

!!! Note
You do not need to add any custom rules for Coil if you use R8, which is the default code shrinker on Android. The rules are added automatically.


Getting Started (`docs/getting_started.md`)

Getting Started

Compose UI

A typical Compose UI project will want to import:

kotlin
implementation("io.coil-kt.coil3:coil-compose:3.6.3")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.6.3")

After that's imported you can load images from the network using AsyncImage:

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

!!! Note
If you use Compose Multiplatform, you'll need to use Ktor instead of OkHttp. See here for how to do that.

Android Views

If you use Android Views instead of Compose UI import:

kotlin
implementation("io.coil-kt.coil3:coil:3.6.3")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.6.3")

After that's imported you can load images from the network using the ImageView.load extension function:

kotlin
imageView.load("https://example.com/image.jpg")

Configuring the singleton ImageLoader

By default, Coil includes a singleton ImageLoader. The ImageLoader executes incoming ImageRequests by fetching, decoding, caching, and returning the result. You don't need to configure your ImageLoader; if you don't Coil will create the singleton ImageLoader with the default configuration.

You can configure it a number of ways (choose only one):

  • Call setSingletonImageLoaderFactory near the entrypoint to your app (the root @Composable of your app). This works best for Compose Multiplatform apps.
kotlin
setSingletonImageLoaderFactory { context ->
    ImageLoader.Builder(context)
        .crossfade(true)
        .build()
}
  • Implement SingletonImageLoader.Factory on your Application in Android. This works best for Android apps.
kotlin
class CustomApplication : Application(), SingletonImageLoader.Factory {
    override fun newImageLoader(context: Context): ImageLoader {
        return ImageLoader.Builder(context)
            .crossfade(true)
            .build()
    }
}
  • Call SingletonImageLoader.setSafe near the entrypoint to your app (e.g. in Application.onCreate on Android). This is the most flexible.
kotlin
SingletonImageLoader.setSafe { context ->
    ImageLoader.Builder(context)
        .crossfade(true)
        .build()
}

!!! Note
If you are writing a library that depends on Coil you should NOT get/set the singleton ImageLoader. Instead, you should depend on io.coil-kt.coil3:coil-core, create your own ImageLoader, and pass it around manually. If you set the singleton ImageLoader in your library you could be overwriting the ImageLoader set by the app using your library if they also use Coil.

Images

To support multiplatform rendering, Coil 3.x uses a custom coil3.Image class. It replaces Android's Drawable, but is fully interoperable with it:

kotlin
val drawable = image.asDrawable(resources)
val image = drawable.asImage()

Coil also defines a coil3.Bitmap class, which is a type alias for android.graphics.Bitmap on Android or org.jetbrains.skia.Bitmap on non-Android platforms:

kotlin
val bitmap = image.toBitmap()
val image = bitmap.asImage()

It's also interoperable with Compose UI's Painter class. This extension function requires importing the coil-compose-core artifact:

kotlin
val painter = image.asPainter()

!!! Note
Painters can't be converted to Images as painters can only be rendered inside a composition whereas Images must be able to be rendered on any Canvas.

Artifacts

Here's a list of the main artifacts Coil has published to mavenCentral():

  • io.coil-kt.coil3:coil: The default artifact which depends on io.coil-kt.coil3:coil-core. It includes a singleton ImageLoader and related extension functions.
  • io.coil-kt.coil3:coil-core: A subset of io.coil-kt.coil3:coil which does not include the singleton ImageLoader and related extension functions.
  • io.coil-kt.coil3:coil-compose: The default Compose UI artifact which depends on io.coil-kt.coil3:coil and io.coil-kt.coil3:coil-compose-core. It includes overloads for AsyncImage, rememberAsyncImagePainter, and SubcomposeAsyncImage that use the singleton ImageLoader.
  • io.coil-kt.coil3:coil-compose-core: A subset of io.coil-kt.coil3:coil-compose which does not include functions that depend on the singleton ImageLoader.
  • io.coil-kt.coil3:coil-network-okhttp: Includes support for fetching images from the network using OkHttp.
  • io.coil-kt.coil3:coil-network-ktor2: Includes support for fetching images from the network using Ktor 2.
  • io.coil-kt.coil3:coil-network-ktor3: Includes support for fetching images from the network using Ktor 3.
  • io.coil-kt.coil3:coil-network-cache-control: Includes support for respecting Cache-Control headers when fetching images from the network.
  • io.coil-kt.coil3:coil-gif: Includes two decoders to support decoding GIFs. See GIFs for more details.
  • io.coil-kt.coil3:coil-svg: Includes a decoder to support decoding SVGs. See SVGs for more details.
  • io.coil-kt.coil3:coil-video: Includes a decoder to support decoding frames from any of Android's supported video formats. See videos for more details.
  • io.coil-kt.coil3:coil-test: Includes classes to support testing. See testing for more details.
  • io.coil-kt.coil3:coil-bom: Includes a bill of materials. Importing coil-bom allows you to depend on other Coil artifacts without specifying a version.

Image Loaders (`docs/image_loaders.md`)

Image Loaders

ImageLoaders are service objects that execute ImageRequests. They handle caching, data fetching, image decoding, request management, memory management, and more.

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, disk cache, and OkHttpClient.

Singleton

The default io.coil-kt.coil3:coil artifact comes with a singleton ImageLoader. Coil creates this ImageLoader lazily. It can be configured a number of ways:

kotlin
// The setSafe method ensures that it won't overwrite an
// existing image loader that's been created.
SingletonImageLoader.setSafe {
    ImageLoader.Builder(context)
        .crossfade(true)
        .build()
}

// An alias of SingletonImageLoader.setSafe that's useful for
// Compose Multiplatform apps.
setSingletonImageLoaderFactory { context ->
    ImageLoader.Builder(context)
        .crossfade(true)
        .build()
}

// Should only be used in tests. If you call this method
// multiple times it will create multiple image loaders.
SingletonImageLoader.setUnsafe {
    ImageLoader.Builder(context)
        .crossfade(true)
        .build()
}

// On Android you can implement SingletonImageLoader.Factory on your
// Application class to have it create the singleton image loader.
class CustomApplication : SingletonImageLoader.Factory {
    override fun newImageLoader(context: Context): ImageLoader {
        return ImageLoader.Builder(context)
            .crossfade(true)
            .build()
    }
}

In all cases ensure the above methods should be invoked as soon as possible when your app starts (i.e. inside Application.onCreate or inside MainActivity.onCreate if your app is only a single Activity.)

Dependency injection

If you have a larger app or want to manage your own ImageLoaders you can depend on io.coil-kt.coil3:coil-core instead of io.coil-kt.coil3:coil.

This route makes scoping the lifecycle of a fake ImageLoader much easier and will overall make testing easier.

Caching

Each ImageLoader keeps a memory cache of recently decoded Bitmaps as well as a disk cache for any images loaded from the Internet. Both can be configured when creating an ImageLoader:

kotlin
val imageLoader = ImageLoader.Builder(context)
    .memoryCache {
        MemoryCache.Builder()
            .maxSizePercent(context, 0.25)
            .build()
    }
    .diskCache {
        DiskCache.Builder()
            .directory(context.cacheDir.resolve("image_cache"))
            .maxSizePercent(0.02)
            .build()
    }
    .build()

You can access items in the memory and disk caches using their keys, which are returned in an ImageResult after an image is loaded. The ImageResult is returned by ImageLoader.execute or in ImageRequest.Listener.onSuccess and ImageRequest.Listener.onError.


Image Pipeline (`docs/image_pipeline.md`)

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:

kotlin
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:

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.asImage(),
                request = chain.request,
                dataSource = DataSource.MEMORY_CACHE,
            )
        }
        return chain.proceed()
    }
}

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:

kotlin
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:

kotlin
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:

kotlin
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:

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)

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

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()
            }
        } else {
            return chain.proceed()
        }
    }
}

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

kotlin
AsyncImage(
    model = ImageRequest.Builder(LocalPlatformContext.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:

kotlin
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:

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

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

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

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


Image Requests (`docs/image_requests.md`)

Image Requests

ImageRequests are value objects that provide all the necessary information for an ImageLoader to load an image. ImageRequests can be created using a builder:

kotlin
val request = ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    .crossfade(true)
    .target(imageView)
    .build()

Once you've created a request pass it to an ImageLoader to enqueue/execute it:

kotlin
imageLoader.enqueue(request)

See the API documentation for more information.

!!! Notes
In Coil 3.x ImageRequest's platform-specific functions (e.g. ImageRequest.Builder.target(ImageView)) are implemented as extension functions and need to be imported separately.


Java Compatibility (`docs/java_compatibility.md`)

Java Compatibility

Coil's API is designed to be Kotlin-first. It leverages Kotlin language features such as inlined lambdas, receiver params, default arguments, and extension functions, which are not available in Java.

Importantly, suspend functions cannot be implemented in Java. This means custom Transformations, Size Resolvers, Fetchers, and Decoders must be implemented in Kotlin.

Despite these limitations, most of Coil's API is Java compatible. You can get the singleton ImageLoader using:

java
ImageLoader imageLoader = SingletonImageLoader.get(context);

The syntax to enqueue an ImageRequest is almost the same in Java and Kotlin:

java
ImageRequest request = new ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    .crossfade(true)
    .target(new ImageViewTarget(imageView))
    .build();
imageLoader.enqueue(request);

!!! Note
ImageView.load cannot be used from Java. Use the ImageRequest.Builder API instead.

suspend functions cannot be easily called from Java. Thus, to get an image synchronously you'll have to use the ImageLoader.executeBlocking extension function which can be called from Java like so:

java
ImageRequest request = new ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    .size(1080, 1920)
    .build();
Image image = ImageLoaders.executeBlocking(imageLoader, request).getImage();
Drawable drawable = Image_androidKt.asDrawable(image, context. resources);

!!! Note
ImageLoaders.executeBlocking will block the current thread instead of suspending. Do not call this from the main thread.


Migrating (`docs/migrating.md`)

Migrating from Glide/Picasso

Here are a few examples of how to migrate Glide/Picasso calls into Coil calls:

Basic Usage

kotlin
// Glide
Glide.with(context)
    .load(url)
    .into(imageView)

// Picasso
Picasso.get()
    .load(url)
    .into(imageView)

// Coil
imageView.load(url)

Custom Requests

kotlin
imageView.scaleType = ImageView.ScaleType.FIT_CENTER

// Glide
Glide.with(context)
    .load(url)
    .placeholder(placeholder)
    .fitCenter()
    .into(imageView)

// Picasso
Picasso.get()
    .load(url)
    .placeholder(placeholder)
    .fit()
    .into(imageView)

// Coil (automatically detects the scale type)
imageView.load(url) {
    placeholder(placeholder)
}

Non-View Targets

kotlin
// Glide (has optional callbacks for start and error)
Glide.with(context)
    .load(url)
    .into(object : CustomTarget<Drawable>() {
        override fun onResourceReady(resource: Drawable, transition: Transition<Drawable>) {
            // Handle the successful result.
        }

        override fun onLoadCleared(placeholder: Drawable) {
            // Remove the drawable provided in onResourceReady from any Views and ensure no references to it remain.
        }
    })

// Picasso
Picasso.get()
    .load(url)
    .into(object : BitmapTarget {
        override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
            // Handle the successful result.
        }

        override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) {
            // Handle the error drawable.
        }

        override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
            // Handle the placeholder drawable.
        }
    })

// Coil
val request = ImageRequest.Builder(context)
    .data(url)
    .target(
        onStart = { placeholder ->
            // Handle the placeholder image.
        },
        onSuccess = { result ->
            // Handle the successful result.
        },
        onError = { error ->
            // Handle the error image.
        }
    )
    .build()
context.imageLoader.enqueue(request)

Background Thread

kotlin
// Glide (blocks the current thread; must not be called from the main thread)
val drawable = Glide.with(context)
    .load(url)
    .submit(width, height)
    .get()

// Picasso (blocks the current thread; must not be called from the main thread)
val drawable = Picasso.get()
    .load(url)
    .resize(width, height)
    .get()

// Coil (suspends, non-blocking, and thread safe)
val request = ImageRequest.Builder(context)
    .data(url)
    .size(width, height)
    .build()
val drawable = context.imageLoader.execute(request).image.asDrawable(resources)

Recipes (`docs/recipes.md`)

Recipes

This page provides guidance on how to handle some common use cases with Coil. You might have to modify this code to fit your exact requirements, but it should hopefully give you a push in the right direction!

See a common use case that isn't covered? Feel free to submit a PR with a new section.

Palette

Palette allows you to extract prominent colors from an image. To create a Palette, you'll need access to an image's Bitmap. This can be done in a number of ways:

You can get access to an image's bitmap by setting a ImageRequest.Listener and enqueuing an ImageRequest:

kotlin
imageView.load("https://example.com/image.jpg") {
    // Disable hardware bitmaps as Palette needs to read the image's pixels.
    allowHardware(false)
    listener(
        onSuccess = { _, result ->
            // Create the palette on a background thread.
            Palette.Builder(result.image.toBitmap()).generate { palette ->
                // Consume the palette.
            }
        }
    )
}

Using a Memory Cache Key as a Placeholder

Using a previous request's MemoryCache.Key as a placeholder for a subsequent request can be useful if the two images are the same, though loaded at different sizes. For instance, if the first request loads the image at 100x100 and the second request loads the image at 500x500, we can use the first image as a synchronous placeholder for the second request.

Here's what this effect looks like in the sample app:

text
<video width="360" height="640" autoplay loop muted playsinline>
    
</video>

Images in the list have intentionally been loaded with very low detail and the crossfade is slowed down to highlight the visual effect.

To achieve this effect, use the MemoryCache.Key of the first request as the ImageRequest.placeholderMemoryCacheKey of the second request. Here's an example:

kotlin
// First request
listImageView.load("https://example.com/image.jpg")

// Second request (once the first request finishes)
detailImageView.load("https://example.com/image.jpg") {
    placeholderMemoryCacheKey(listImageView.result.memoryCacheKey)
}

Compose AnimatedContent

Use rememberAsyncImagePainter and AnimatedContent to animate between a placeholder and the loaded image:

kotlin
val sizeResolver = rememberConstraintsSizeResolver()
val painter = rememberAsyncImagePainter(
    model = ImageRequest.Builder(LocalPlatformContext.current)
        .data("https://example.com/image.jpg")
        .size(sizeResolver)
        .build(),
)
val state by painter.state.collectAsState()

AnimatedContent(
    modifier = Modifier.then(sizeResolver),
    targetState = state,
    contentKey = { it::class },
    transitionSpec = { fadeIn() togetherWith fadeOut() },
) { targetState ->
    when (targetState) {
        is AsyncImagePainter.State.Empty,
        is AsyncImagePainter.State.Loading -> PlaceholderContent()
        is AsyncImagePainter.State.Success -> {
            // Keep the old image visible while it animates out after the model changes.
            Image(
                painter = targetState.painter,
                contentDescription = stringResource(R.string.description),
            )
        }
        is AsyncImagePainter.State.Error -> ErrorContent()
    }
}

Pass targetState.painter to Image so an old image remains visible while it animates out after the model changes.

The final image size is unknown until loading finishes. Use fixed constraints or a known aspect ratio to reserve space.

rememberAsyncImagePainter starts in State.Empty, even when the image is in the memory cache. Use state.result.dataSource to skip an animation for memory-cached images if needed.

NOTE: AnimatedContent is much more expensive than a painter crossfade and keeps the old image in memory until the animation ends. Prefer ImageRequest.Builder.crossfade in lazy lists or for a simple fade.

Shared Element Transitions

Shared element transitions allow you to animate between Activities and Fragments. Here are some recommendations on how to get them to work with Coil:

  • Shared element transitions are incompatible with hardware bitmaps. You should set allowHardware(false) to disable hardware bitmaps for both the ImageView you are animating from and the view you are animating to. If you don't, the transition will throw an java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps exception.

  • Use the MemoryCache.Key of the start image as the placeholderMemoryCacheKey for the end image. This ensures that the start image is used as the placeholder for the end image, which results in a smooth transition with no white flashes if the image is in the memory cache.

  • Use ChangeImageTransform and ChangeBounds together for optimal results.

Using Compose? Check out this article for how to perform shared element transitions with AsyncImage.

Remote Views

Coil does not provide a Target for RemoteViews out of the box, however you can create one like so:

kotlin
class RemoteViewsTarget(
    private val context: Context,
    private val componentName: ComponentName,
    private val remoteViews: RemoteViews,
    @IdRes private val imageViewResId: Int
) : Target {

    override fun onStart(placeholder: Image?) = setImage(placeholder)

    override fun onError(error: Image?) = setImage(error)

    override fun onSuccess(result: Image) = setImage(result)

    private fun setImage(image: Image?) {
        remoteViews.setImageViewBitmap(imageViewResId, image?.toBitmap())
        AppWidgetManager.getInstance(context).updateAppWidget(componentName, remoteViews)
    }
}

Then enqueue/execute the request like normal:

kotlin
val request = ImageRequest.Builder(context)
    .data("https://example.com/image.jpg")
    .target(RemoteViewsTarget(context, componentName, remoteViews, imageViewResId))
    .build()
imageLoader.enqueue(request)

Transforming Painters

Both AsyncImage and AsyncImagePainter have placeholder/error/fallback arguments that accept Painters. Painters are less flexible than using composables, but are faster as Coil doesn't need to use subcomposition. That said, it may be necessary to inset, stretch, tint, or transform your painter to get the desired UI. To accomplish this, copy this Gist into your project and wrap the painter like so:

kotlin
AsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = null,
    placeholder = forwardingPainter(
        painter = painterResource(R.drawable.placeholder),
        colorFilter = ColorFilter(Color.Red),
        alpha = 0.5f,
    ),
)

The onDraw can be overwritten using a trailing lambda:

kotlin
AsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = null,
    placeholder = forwardingPainter(painterResource(R.drawable.placeholder)) { info ->
        inset(50f, 50f) {
            with(info.painter) {
                draw(size, info.alpha, info.colorFilter)
            }
        }
    },
)

Transforming Requests

You may need to transform HTTP requests used to fetch images. In this example, we'll use an Interceptor to append width & height query parameters to the request URL.

kotlin
class UrlSizeInterceptor : Interceptor {

    override suspend fun intercept(chain: Chain): ImageResult {
        val request = chain.request
        val uri = request.uri

        if (uri == null || uri.scheme !in setOf("https", "http")) {
            // Ignore non-HTTP requests.
            return chain.proceed()
        }

        val (width, height) = chain.size
        return if (width is Pixels && height is Pixels) {
            val transformUri = uri.newBuilder()
                .query("width=${width.px}&height=${height.px}")
                .build()

            val transformedRequest = request.newBuilder()
                .data(transformUri)
                .build()
            return chain.withRequest(transformedRequest).proceed()
        } else {
            // Width & height aren't available, i.e. because of infinite constraints.
            chain.proceed()
        }
    }

    private val ImageRequest.uri: Uri?
        get() = when (val data = data) {
            is Uri -> data
            is coil3.Uri -> data.toAndroidUri()
            is String -> data.toUri()
            else -> null
        }
}

Don't forget to register add interceptor to your ImageLoader!

kotlin
ImageLoader.Builder(context)
    .components {
        add(UrlSizeInterceptor())
    }
    .build()

Upgrading To Coil2 (`docs/upgrading_to_coil2.md`)

Upgrading to Coil 2.x

This is a short guide to highlight the main changes when upgrading from Coil 1.x to 2.x and how to handle them. This upgrade guide doesn't cover every binary or source incompatible change, but it does cover the most important changes.

Minimum API 21

Coil 2.x requires minimum API 21. This is also the minimum API required for Compose and OkHttp 4.x.

ImageRequest default scale

Coil 2.x changes ImageRequest's default scale from Scale.FILL to Scale.FIT. This was done to be consistent with ImageView's default ScaleType and Image's default ContentScale. Scale is still autodetected if you set an ImageView as your ImageRequest.target.

Size refactor

Size's width and height are now two Dimensions instead of Int pixel values. Dimension is either a pixel value or Dimension.Undefined, which represents an undefined/unbounded constraint. For example, if the size is Size(400, Dimension.Undefined) that means the image should be scaled to have 400 pixels for its width irrespective of its height. You can use the pxOrElse extension to get the pixel value (if present), else use a fallback:

kotlin
val width = size.width.pxOrElse { -1 }
if (width > 0) {
    // Use the pixel value.
}

This change was made to improve support for cases where a target has one unbounded dimension (e.g. if one dimension is ViewGroup.LayoutParams.WRAP_CONTENT for a View or Constraints.Infinity in Compose).

Compose

Coil 2.x significantly reworks the Compose integration to add features, improve stability, and improve performance.

In Coil 1.x you would use rememberImagePainter to load an image:

kotlin
val painter = rememberImagePainter("https://example.com/image.jpg") {
    crossfade(true)
}

Image(
    painter = painter,
    contentDescription = null,
    contentScale = ContentScale.Crop
)

In Coil 2.x rememberImagePainter has been changed to rememberAsyncImagePainter with the following changes:

  • The trailing lambda argument to configure the ImageRequest has been removed.
  • In Coil 2.x, rememberAsyncImagePainter defaults to using ContentScale.Fit to be consistent with Image whereas in Coil 1.x it would default to ContentScale.Crop. As such, if you set a custom ContentScale on Image, you now also need to pass it to rememberAsyncImagePainter.
kotlin
val painter = rememberAsyncImagePainter(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/image.jpg")
        .crossfade(true)
        .build(),
    contentScale = ContentScale.Crop
)

Image(
    painter = painter,
    contentDescription = null,
    contentScale = ContentScale.Crop
)

Additionally, Coil now has AsyncImage and SubcomposeAsyncImage composable functions, which add new features and work-around some design limitations of rememberAsyncImagePainter. Check out the full Compose docs here.

Disk Cache

Coil 2.x has its own public disk cache class that can be accessed using imageLoader.diskCache. Coil 1.x relied on OkHttp's disk cache, however it's no longer needed.

To configure the disk cache in 1.x you would use CoilUtils.createDefaultCache:

kotlin
ImageLoader.Builder(context)
    .okHttpClient {
        OkHttpClient.Builder().cache(CoilUtils.createDefaultCache(context)).build()
    }
    .build()

In Coil 2.x you should not set a Cache object on your OkHttpClient when used with an ImageLoader. Instead configure the disk cache object like so:

kotlin
ImageLoader.Builder(context)
    .diskCache {
        DiskCache.Builder()
            .directory(context.cacheDir.resolve("image_cache"))
            .build()
    }
    .build()

This change was made to add functionality and improve performance:

  • Support thread interruption while decoding images.
    • Thread interruption allows fast cancellation of decode operations. This is particularly important for quickly scrolling through a list.
    • By using a custom disk cache Coil is able to ensure a network source is fully read to disk before decoding. This is necessary as writing the data to disk cannot be interrupted - only the decode step can be interrupted. OkHttp's Cache shouldn't be used with Coil 2.0 as it's not possible to guarantee that all data is written to disk before decoding.
  • Avoid buffering/creating temporary files for decode APIs that don't support InputStreams or require direct access to a File (e.g. ImageDecoder, MediaMetadataRetriever).
  • Add a public read/write DiskCache API.

In Coil 2.x Cache-Control and other cache headers are still supported - except Vary headers, as the cache only checks that the URLs match. Additionally, only responses with a response code in the range [200..300) are cached.

When upgrading from Coil 1.x to 2.x, any existing disk cache will be cleared as the internal format has changed.

Image pipeline refactor

Coil 2.x refactors the image pipeline classes to be more flexible. Here's a high-level list of the changes:

  • Introduce a new class, Keyer, that computes the memory cache key for a request. It replaces Fetcher.key.
  • Mapper, Keyer, Fetcher, and Decoder can return null to delegate to the next element in the list of components.
  • Add Options to Mapper.map's signature.
  • Introduce Fetcher.Factory and Decoder.Factory. Use the factories to determine if a specific Fetcher/Decoder is applicable. Return null if that Fetcher/Decoder is not applicable.

Remove bitmap pooling

Coil 2.x removes bitmap pooling and its associated classes (BitmapPool, PoolableViewTarget). See here for why it was removed.


Upgrading To Coil3 (`docs/upgrading_to_coil3.md`)

Upgrading to Coil 3.x

Coil 3 is the next major version of Coil that has a number of major improvements:

  • Full support for Compose Multiplatform including all major targets (Android, iOS, JVM, JS, and WASM).
  • Support for multiple networking libraries (Ktor and OkHttp). Alternatively, Coil can be used without a network dependency if you only need to load local/static files.
  • Improved Compose @Preview rendering and support for custom preview behavior via LocalAsyncImagePreviewHandler.
  • Important fixes for bugs that required breaking existing behaviour (outlined below).

This document provides a high-level overview of the main changes from Coil 2 to Coil 3 and highlights any breaking or important changes. It does not cover every binary incompatible change or small behaviour changes.

Using Coil 3 in a Compose Multiplatform project? Check out the samples repository for examples.

Maven Coordinates and Package Name

Coil's Maven coordinates were updated from io.coil-kt to io.coil-kt.coil3 and its package name was updated from coil to coil3. This allows Coil 3 to run side by side with Coil 2 without binary compatibility issues. For example, io.coil-kt:coil:2.7.0 is now io.coil-kt.coil3:coil:3.0.0.

The coil-base and coil-compose-base artifacts were renamed to coil-core and coil-compose-core respectively to align with the naming conventions used by Coroutines, Ktor, and AndroidX.

Network Images

coil-core no longer supports loading images from the network by default. You must add a dependency on one of Coil's network artifacts. See here for more info.. This was changed so consumers could use different networking libraries or avoid a network dependency if their app doesn't need it.

Additionally, cache control headers are no longer respected by default. See here for more info.

Multiplatform

Coil 3 is now a Kotlin Multiplatform library that supports Android, JVM, iOS, macOS, Javascript, and WASM.

On Android, Coil uses the standard graphics classes to render images. On non-Android platforms, Coil uses Skiko to render images. Skiko is a set of Kotlin bindings that wrap the Skia graphics engine developed by Google.

As part of decoupling from the Android SDK, a number of API changes were made. Notably:

  • Drawable was replaced with a custom Image interface. Use Drawable.asImage() and Image.asDrawable(resources) to convert between the classes on Android. On non-Android platforms use Bitmap.asImage() and Image.toBitmap().
  • Usages of Android's android.net.Uri class were replaced a multiplatform coil3.Uri class. Any call sites that pass android.net.Uri as ImageRequest.data are unaffected. Custom Fetchers that rely on receiving an android.net.Uri will need to be updated to use coil3.Uri.
  • Usages of Context were replaced with PlatformContext. PlatformContext is a type alias for Context on Android and can be accessed using PlatformContext.INSTANCE on non-Android platforms. Use LocalPlatformContext.current to get a reference in Compose Multiplatform.
  • The Coil class was renamed to SingletonImageLoader.
  • If you're implementing ImageLoaderFactory in your custom Android Application class, you'll need to switch to implementing SingletonImageLoader.Factory as a replacement for ImageLoaderFactory. Once you implement SingletonImageLoader.Factory, you'll be able to override newImageLoader() if you need or want to override it.

The coil-svg artifact is supported in multiplatform, but the coil-gif and coil-video artifacts continue to be Android-only (for now) as they rely on specific Android decoders and libraries.

Compose

The coil-compose artifact's APIs are mostly unchanged. You can continue using AsyncImage, SubcomposeAsyncImage, and rememberAsyncImagePainter the same way as with Coil 2. Additionally, this methods have been updated to be restartable and skippable which should improve their performance.

  • AsyncImagePainter.state is now a StateFlow. It should be observed using val state = painter.state.collectAsState().
  • AsyncImagePainter's default SizeResolver no longer waits for the first onDraw call to get the size of the canvas. Instead, AsyncImagePainter defaults to Size.ORIGINAL.
  • The Compose modelEqualityDelegate delegate is now set via a composition local, LocalAsyncImageModelEqualityDelegate, instead of as a parameter to AsyncImage/SubcomposeAsyncImage/rememberAsyncImagePainter.

General

Other important behavior changes include:

  • First party Fetchers and Decoders (e.g. NetworkFetcher.Factory, SvgDecoder, etc.) are now automatically added to each new ImageLoader through a service loader. This behaviour can be disabled with ImageLoader.Builder.serviceLoaderEnabled(false).
  • Remove support for android.resource://example.package.name/drawable/image URIs as it prevents resource shrinking optimizations. It's recommended to pass R.drawable.image values directly. Passing the resource ID instead of the resource name will still work: android.resource://example.package.name/12345678. If you still needs its functionality you can manually include ResourceUriMapper in your component registry.
  • A file's last write timestamp is no longer added to its cache key by default. This is to avoid reading the disk on the main thread (even for a very short amount of time). This can be re-enabled with ImageRequest.Builder.addLastModifiedToFileCacheKey(true) or ImageLoader.Builder.addLastModifiedToFileCacheKey(true).
  • Output image dimensions are now enforced to be less than 4096x4096 to guard against accidental OOMs. This can be configured with ImageLoader/ImageRequest.Builder.maxBitmapSize. To disable this behavior set maxBitmapSize to Size.ORIGINAL.
  • Coil 2's Parameters API was replaced by Extras. Extras don't require a string key and instead rely on identity equality. Extras don't support modifying the memory cache key. Instead, use ImageRequest.memoryCacheKeyExtra if your extra affects the memory cache key.
  • Many ImageRequest.Builder functions have moved to be extension functions to more easily support multiplatform.

Works With Coil (`docs/works_with_coil.md`)

Works with Coil

A collection of third party libraries that work nicely with Coil.

  • Telephoto: Building blocks for designing media experiences in Compose UI.
  • Landscapist: A pluggable, highly optimized Jetpack Compose and Kotlin Multiplatform image loading library that fetches and displays network images with Glide, Coil, and Fresco.
  • coil-resvg: Render SVGs with resvg in Coil.

Have a library that works with or builds on top of Coil? Submit a PR to include it here!


--- METRICS ---