Skip to content

Compose

To add support for Compose UI, import the extension library:

implementation("io.coil-kt.coil3:coil-compose:3.0.4")

Then use the AsyncImage composable to load and display an image:

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

model can either be the ImageRequest.data value - or the ImageRequest itself. contentDescription sets the text used by accessibility services to describe what this image represents.

Note

If you use Compose on JVM/desktop you should import org.jetbrains.kotlinx:kotlinx-coroutines-swing:<coroutines-version>. Coil relies on Dispatchers.Main.immediate to resolve images from the memory cache synchronously and kotlinx-coroutines-swing provides support for that on JVM (non-Android) platforms.

AsyncImage

AsyncImage is a composable that executes an image request asynchronously and renders the result. It supports the same arguments as the standard Image composable and additionally, it supports setting placeholder/error/fallback painters and onLoading/onSuccess/onError callbacks. Here's an example that loads an image with a circle crop, crossfade, and sets a placeholder:

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/image.jpg")
        .crossfade(true)
        .build(),
    placeholder = painterResource(R.drawable.placeholder),
    contentDescription = stringResource(R.string.description),
    contentScale = ContentScale.Crop,
    modifier = Modifier.clip(CircleShape),
)

When to use this function:

Prefer using AsyncImage in most cases. It correctly determines the size your image should be loaded at based on the constraints of the composable and the provided ContentScale.

rememberAsyncImagePainter

Internally, AsyncImage and SubcomposeAsyncImage use rememberAsyncImagePainter to load the model. If you need a Painter and not a composable, you can load the image using rememberAsyncImagePainter:

val painter = rememberAsyncImagePainter("https://example.com/image.jpg")

rememberAsyncImagePainter is more flexible than AsyncImage and SubcomposeAsyncImage, but has a couple drawbacks (see below).

When to use this function:

Useful if you need a Painter instead of a composable - or if you need to observe the AsyncImagePainter.state and draw a different composable based on it - or if you need to manually restart the image request using AsyncImagePainter.restart.

The main drawback of this function is it does not detect the size your image is loaded at on screen and always loads the image with its original dimensions. You can pass a custom SizeResolver or use rememberConstraintsSizeResolver (which is what AsyncImage uses internally) to resolve this. Example:

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

Image(
    painter = painter,
    contentDescription = null,
    modifier = Modifier.then(sizeResolver),
)

Another drawback is AsyncImagePainter.state will always be AsyncImagePainter.State.Empty for the first composition when using rememberAsyncImagePainter - even if the image is present in the memory cache and it will be drawn in the first frame.

SubcomposeAsyncImage

SubcomposeAsyncImage is a variant of AsyncImage that uses subcomposition to provide a slot API for AsyncImagePainter's states instead of using Painters. Here's an example:

SubcomposeAsyncImage(
    model = "https://example.com/image.jpg",
    loading = {
        CircularProgressIndicator()
    },
    contentDescription = stringResource(R.string.description),
)

Additionally, you can have more complex logic using its content argument and SubcomposeAsyncImageContent, which renders the current state:

SubcomposeAsyncImage(
    model = "https://example.com/image.jpg",
    contentDescription = stringResource(R.string.description)
) {
    val state by painter.state.collectAsState()
    if (state is AsyncImagePainter.State.Success) {
        SubcomposeAsyncImageContent()
    } else {
        CircularProgressIndicator()
    }
}

Note

Subcomposition is slower than regular composition so this composable may not be suitable for performance-critical parts of your UI (e.g. LazyList).

When to use this function:

Generally prefer using rememberAsyncImagePainter instead of this function if you need to observe AsyncImagePainter.state as it does not use subcomposition.

Specifically, this function is only useful if you need to observe AsyncImagePainter.state and you can't have it be Empty for the first composition and first frame like with rememberAsyncImagePainter. SubcomposeAsyncImage uses subcomposition to get the image's constraints so it's AsyncImagePainter.state is up to date immediately.

Observing AsyncImagePainter.state

Example:

val painter = rememberAsyncImagePainter("https://example.com/image.jpg")

when (painter.state) {
    is AsyncImagePainter.State.Empty,
    is AsyncImagePainter.State.Loading -> {
        CircularProgressIndicator()
    }
    is AsyncImagePainter.State.Success -> {
        Image(
            painter = painter,
            contentDescription = stringResource(R.string.description)
        )
    }
    is AsyncImagePainter.State.Error -> {
        // Show some error UI.
    }
}

Transitions

You can enable the built in crossfade transition using ImageRequest.Builder.crossfade:

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/image.jpg")
        .crossfade(true)
        .build(),
    contentDescription = null,
)

Custom Transitions do not work with AsyncImage, SubcomposeAsyncImage, or rememberAsyncImagePainter as they require a View reference. CrossfadeTransition works due to special internal support.

That said, it's possible to create custom transitions in Compose by observing AsyncImagePainter.state:

val painter = rememberAsyncImagePainter("https://example.com/image.jpg")

val state by painter.state.collectAsState()
if (state is AsyncImagePainter.State.Success && state.result.dataSource != DataSource.MEMORY_CACHE) {
    // Perform the transition animation.
}

Image(
    painter = painter,
    contentDescription = stringResource(R.string.description),
)

Previews

The Android Studio preview behaviour for AsyncImage/rememberAsyncImagePainter/SubcomposeAsyncImage is controlled by the LocalAsyncImagePreviewHandler. By default, it will attempt to perform the request as normal inside the preview environment. Network access is disabled in the preview environment so network URLs will always fail.

You can override the preview behaviour like so:

val previewHandler = AsyncImagePreviewHandler {
    FakeImage(color = 0xFFFF0000) // Available in `io.coil-kt.coil3:coil-test`.
}

CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) {
    AsyncImage(
        model = "https://example.com/image.jpg",
        contentDescription = null,
    )
}

This is also useful for AndroidX's Compose Preview Screenshot Testing library, which executes in the same preview environment.

Compose Multiplatform Resources

Coil supports loading Compose Multiplatform Resources by using Res.getUri as the model parameter. Example:

AsyncImage(
    model = Res.getUri("drawable/sample.jpg"),
    contentDescription = null,
)

Note

Res.drawable.image and other compile-safe references are not supported by Coil; you must use Res.getUri("drawable/image") instead. It's not possible for Coil to support this until Compose Multiplatform exposes APIs to support it. Follow this ticket.