What This Skill Provides
The Coil Compose skill provides expert guidance on using Coil (Coroutines Image Loader) for image loading in Jetpack Compose. Coil is the recommended library for Compose due to its efficiency, coroutine-based architecture, and seamless integration with Compose.
When to Use This Skill
Loading images from URLs or URIs
Handling image loading states (loading, success, error)
Optimizing image performance in Compose
Displaying placeholders and error images
Applying transformations to images
Building image-heavy UIs with LazyColumn/LazyRow
Setup
Add Coil to your project:
dependencies {
implementation ( "io.coil-kt:coil-compose:2.5.0" )
}
Primary Component: AsyncImage
Use AsyncImage for most use cases. It handles size resolution automatically and supports standard Image parameters.
AsyncImage (
model = "https://example.com/image.jpg" ,
contentDescription = "Product image" ,
modifier = Modifier. size ( 200 .dp)
)
With Image Request Builder
AsyncImage with ImageRequest
AsyncImage (
model = ImageRequest. Builder (LocalContext.current)
. data ( "https://example.com/image.jpg" )
. crossfade ( true )
. build (),
placeholder = painterResource (R.drawable.placeholder),
error = painterResource (R.drawable.error),
contentDescription = stringResource (R.string.product_description),
contentScale = ContentScale.Crop,
modifier = Modifier
. size ( 200 .dp)
. clip (CircleShape)
)
AsyncImage Benefits:
Automatically detects the size your image is loaded at
Loads images with appropriate dimensions for better performance
Supports all standard Image parameters
Simplest API for most use cases
Content Scale Options
AsyncImage (
model = imageUrl,
contentDescription = null ,
contentScale = ContentScale.Crop,
modifier = Modifier. size ( 200 .dp)
)
Scales and crops the image to fill the bounds. AsyncImage (
model = imageUrl,
contentDescription = null ,
contentScale = ContentScale.Fit,
modifier = Modifier. size ( 200 .dp)
)
Scales the image to fit inside the bounds while maintaining aspect ratio. AsyncImage (
model = imageUrl,
contentDescription = null ,
contentScale = ContentScale.FillWidth,
modifier = Modifier. fillMaxWidth ()
)
Scales the image to fill the width of the bounds.
Advanced: SubcomposeAsyncImage
Use SubcomposeAsyncImage when you need custom slot APIs for different states (Loading, Success, Error).
SubcomposeAsyncImage (
model = "https://example.com/image.jpg" ,
contentDescription = "Product image" ,
loading = {
Box (
modifier = Modifier. fillMaxSize (),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator ()
}
},
error = {
Box (
modifier = Modifier. fillMaxSize (),
contentAlignment = Alignment.Center
) {
Icon (
imageVector = Icons.Default.Error,
contentDescription = "Error loading image" ,
tint = MaterialTheme.colorScheme.error
)
}
},
modifier = Modifier. size ( 200 .dp)
)
Performance Warning Subcomposition is slower than regular composition. Avoid using SubcomposeAsyncImage in performance-critical areas like LazyColumn or LazyRow. Use AsyncImage with placeholder and error parameters instead.
Low-Level Control: rememberAsyncImagePainter
Use rememberAsyncImagePainter only when you need a Painter instead of a composable (e.g., for Canvas or Icon).
val painter = rememberAsyncImagePainter (
model = ImageRequest. Builder (LocalContext.current)
. data ( "https://example.com/image.jpg" )
. size (Size.ORIGINAL)
. build ()
)
Image (
painter = painter,
contentDescription = "Custom image" ,
modifier = Modifier. size ( 200 .dp)
)
Size Detection Warning rememberAsyncImagePainter does not detect the size your image is loaded at on screen and always loads the image with its original dimensions by default. This can cause performance issues.Use AsyncImage unless a Painter is strictly required.
Circular Images
Using Modifier
Using Transformation
AsyncImage (
model = userAvatarUrl,
contentDescription = "User avatar" ,
contentScale = ContentScale.Crop,
modifier = Modifier
. size ( 64 .dp)
. clip (CircleShape)
)
Rounded Corners
AsyncImage (
model = imageUrl,
contentDescription = null ,
contentScale = ContentScale.Crop,
modifier = Modifier
. size ( 200 .dp)
. clip ( RoundedCornerShape ( 12 .dp))
)
Common Use Cases
User Profile Avatar
@Composable
fun UserAvatar (
avatarUrl: String ?,
userName: String ,
modifier: Modifier = Modifier
) {
AsyncImage (
model = ImageRequest. Builder (LocalContext.current)
. data (avatarUrl)
. crossfade ( true )
. build (),
placeholder = painterResource (R.drawable.default_avatar),
error = painterResource (R.drawable.default_avatar),
contentDescription = " $userName 's avatar" ,
contentScale = ContentScale.Crop,
modifier = modifier
. size ( 48 .dp)
. clip (CircleShape)
)
}
Product Card in LazyColumn
@Composable
fun ProductCard (
product: Product ,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card (
onClick = onClick,
modifier = modifier. fillMaxWidth ()
) {
Row (modifier = Modifier. padding ( 16 .dp)) {
AsyncImage (
model = ImageRequest. Builder (LocalContext.current)
. data (product.imageUrl)
. crossfade ( true )
. build (),
placeholder = painterResource (R.drawable.placeholder),
error = painterResource (R.drawable.error),
contentDescription = product.name,
contentScale = ContentScale.Crop,
modifier = Modifier
. size ( 80 .dp)
. clip ( RoundedCornerShape ( 8 .dp))
)
Spacer (modifier = Modifier. width ( 16 .dp))
Column {
Text (
text = product.name,
style = MaterialTheme.typography.titleMedium
)
Text (
text = product.price,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
For lists, always use AsyncImage with placeholder and error parameters instead of SubcomposeAsyncImage for better performance.
Image Gallery Grid
@Composable
fun ImageGallery (
images: List < String >,
modifier: Modifier = Modifier
) {
LazyVerticalGrid (
columns = GridCells. Fixed ( 3 ),
horizontalArrangement = Arrangement. spacedBy ( 4 .dp),
verticalArrangement = Arrangement. spacedBy ( 4 .dp),
modifier = modifier
) {
items (images) { imageUrl ->
AsyncImage (
model = ImageRequest. Builder (LocalContext.current)
. data (imageUrl)
. crossfade ( true )
. build (),
contentDescription = null ,
contentScale = ContentScale.Crop,
modifier = Modifier
. aspectRatio ( 1f )
. clip ( RoundedCornerShape ( 4 .dp))
)
}
}
}
1. Use Singleton ImageLoader
Use a single ImageLoader instance for the entire app to share disk and memory cache.
class MyApplication : Application () {
override fun onCreate () {
super . onCreate ()
val imageLoader = ImageLoader. Builder ( this )
. crossfade ( true )
. memoryCache {
MemoryCache. Builder ( this )
. maxSizePercent ( 0.25 )
. build ()
}
. diskCache {
DiskCache. Builder ()
. directory (cacheDir. resolve ( "image_cache" ))
. maxSizeBytes ( 512L * 1024 * 1024 ) // 512MB
. build ()
}
. build ()
Coil. setImageLoader (imageLoader)
}
}
2. Enable Crossfade
Always enable crossfade(true) for smoother transitions.
AsyncImage (
model = ImageRequest. Builder (LocalContext.current)
. data (imageUrl)
. crossfade ( true ) // Smooth transition
. build (),
contentDescription = null
)
3. Set Appropriate ContentScale
Ensure contentScale is set appropriately to avoid loading larger images than necessary.
AsyncImage (
model = imageUrl,
contentDescription = null ,
contentScale = ContentScale.Crop, // Crop to fit bounds
modifier = Modifier. size ( 200 .dp)
)
4. Avoid SubcomposeAsyncImage in Lists
DO: Use AsyncImage
DON'T: Use SubcomposeAsyncImage
LazyColumn {
items (products) { product ->
AsyncImage (
model = product.imageUrl,
placeholder = painterResource (R.drawable.placeholder),
error = painterResource (R.drawable.error),
contentDescription = product.name
)
}
}
Implementation Checklist
When to Use Each Component
Component Use Case AsyncImageDefault choice for most use casesSubcomposeAsyncImageCustom loading/error states (avoid in lists) rememberAsyncImagePainterNeed a Painter for Canvas or Icon
References