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:
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:
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null,
)Check out Coil's full documentation here.
License
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:
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:
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:
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:
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:
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:
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:
org.jetbrains.compose.library.compatibility.check.disable=trueThis 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):
allprojects {
repositories {
maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
}
}Gradle Kotlin DSL (.gradle.kts):
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:
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:
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:
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:
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
setSingletonImageLoaderFactorynear the entrypoint to your app (the root@Composableof your app). This works best for Compose Multiplatform apps.
setSingletonImageLoaderFactory { context ->
ImageLoader.Builder(context)
.crossfade(true)
.build()
}- Implement
SingletonImageLoader.Factoryon yourApplicationin Android. This works best for Android apps.
class CustomApplication : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(true)
.build()
}
}- Call
SingletonImageLoader.setSafenear the entrypoint to your app (e.g. inApplication.onCreateon Android). This is the most flexible.
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:
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:
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:
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 onio.coil-kt.coil3:coil-core. It includes a singletonImageLoaderand related extension functions.io.coil-kt.coil3:coil-core: A subset ofio.coil-kt.coil3:coilwhich does not include the singletonImageLoaderand related extension functions.io.coil-kt.coil3:coil-compose: The default Compose UI artifact which depends onio.coil-kt.coil3:coilandio.coil-kt.coil3:coil-compose-core. It includes overloads forAsyncImage,rememberAsyncImagePainter, andSubcomposeAsyncImagethat use the singletonImageLoader.io.coil-kt.coil3:coil-compose-core: A subset ofio.coil-kt.coil3:coil-composewhich does not include functions that depend on the singletonImageLoader.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 respectingCache-Controlheaders 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. Importingcoil-bomallows 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:
// 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:
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:
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.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:
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()
}
} else {
return chain.proceed()
}
}
}Finally, we can set the property when creating our ImageRequest:
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.timeoutextension function we defined. - We can read the timeout inside
Mappers,Fetchers, andDecoders via theOptions.timeoutextension 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.
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:
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:
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:
ImageLoader imageLoader = SingletonImageLoader.get(context);The syntax to enqueue an ImageRequest is almost the same in Java and Kotlin:
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:
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
// Glide
Glide.with(context)
.load(url)
.into(imageView)
// Picasso
Picasso.get()
.load(url)
.into(imageView)
// Coil
imageView.load(url)Custom Requests
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
// 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
// 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:
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:
<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:
// 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:
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 theImageViewyou are animating from and the view you are animating to. If you don't, the transition will throw anjava.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmapsexception.Use the
MemoryCache.Keyof the start image as theplaceholderMemoryCacheKeyfor 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
ChangeImageTransformandChangeBoundstogether 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:
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:
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:
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:
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.
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!
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:
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:
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
ImageRequesthas been removed. - In Coil 2.x,
rememberAsyncImagePainterdefaults to usingContentScale.Fitto be consistent withImagewhereas in Coil 1.x it would default toContentScale.Crop. As such, if you set a customContentScaleonImage, you now also need to pass it torememberAsyncImagePainter.
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:
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:
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
Cacheshouldn'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 aFile(e.g.ImageDecoder,MediaMetadataRetriever). - Add a public read/write
DiskCacheAPI.
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 replacesFetcher.key. Mapper,Keyer,Fetcher, andDecodercan returnnullto delegate to the next element in the list of components.- Add
OptionstoMapper.map's signature. - Introduce
Fetcher.FactoryandDecoder.Factory. Use the factories to determine if a specificFetcher/Decoderis applicable. Returnnullif thatFetcher/Decoderis 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
@Previewrendering and support for custom preview behavior viaLocalAsyncImagePreviewHandler. - 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:
Drawablewas replaced with a customImageinterface. UseDrawable.asImage()andImage.asDrawable(resources)to convert between the classes on Android. On non-Android platforms useBitmap.asImage()andImage.toBitmap().- Usages of Android's
android.net.Uriclass were replaced a multiplatformcoil3.Uriclass. Any call sites that passandroid.net.UriasImageRequest.dataare unaffected. CustomFetchers that rely on receiving anandroid.net.Uriwill need to be updated to usecoil3.Uri. - Usages of
Contextwere replaced withPlatformContext.PlatformContextis a type alias forContexton Android and can be accessed usingPlatformContext.INSTANCEon non-Android platforms. UseLocalPlatformContext.currentto get a reference in Compose Multiplatform. - The
Coilclass was renamed toSingletonImageLoader. - If you're implementing
ImageLoaderFactoryin your custom AndroidApplicationclass, you'll need to switch to implementingSingletonImageLoader.Factoryas a replacement forImageLoaderFactory. Once you implementSingletonImageLoader.Factory, you'll be able to overridenewImageLoader()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.stateis now aStateFlow. It should be observed usingval state = painter.state.collectAsState().AsyncImagePainter's defaultSizeResolverno longer waits for the firstonDrawcall to get the size of the canvas. Instead,AsyncImagePainterdefaults toSize.ORIGINAL.- The Compose
modelEqualityDelegatedelegate is now set via a composition local,LocalAsyncImageModelEqualityDelegate, instead of as a parameter toAsyncImage/SubcomposeAsyncImage/rememberAsyncImagePainter.
General
Other important behavior changes include:
- First party
Fetchers andDecoders (e.g.NetworkFetcher.Factory,SvgDecoder, etc.) are now automatically added to each newImageLoaderthrough a service loader. This behaviour can be disabled withImageLoader.Builder.serviceLoaderEnabled(false). - Remove support for
android.resource://example.package.name/drawable/imageURIs as it prevents resource shrinking optimizations. It's recommended to passR.drawable.imagevalues 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 includeResourceUriMapperin 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)orImageLoader.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 setmaxBitmapSizetoSize.ORIGINAL. - Coil 2's
ParametersAPI was replaced byExtras.Extrasdon't require a string key and instead rely on identity equality.Extrasdon't support modifying the memory cache key. Instead, useImageRequest.memoryCacheKeyExtraif your extra affects the memory cache key. - Many
ImageRequest.Builderfunctions 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 ---
- Files Extracted: 13
- Estimated Token Budget: ~14064 tokens
- Canonical Reference: https://codewiki.google/github.com/coil-kt/coil