0Pricing
Android Academy · Lesson

Placeholders and Error States

Graceful loading and failures.

Placeholders and Error States is a free Android Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Loading Isn't Instant

Network images take time. During that gap your UI shows... nothing, unless you handle it. And sometimes the image never arrives: the URL is broken, the server is down, or the user is offline.

A polished app shows a placeholder while loading and a fallback when things fail, instead of an empty hole or a crash. Coil makes both easy.

In this lesson you'll add placeholders, error images, and custom per-state UI.

A Simple Placeholder

The fastest way to add a placeholder is to pass a drawable to AsyncImage via the placeholder parameter using painterResource. Coil shows it until the real image finishes loading.

Use a neutral, lightweight drawable, such as a gray box or a logo silhouette.

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage

@Composable
fun ImageWithPlaceholder(url: String) {
    AsyncImage(
        model = url,
        contentDescription = null,
        placeholder = painterResource(R.drawable.placeholder_gray)
    )
}

Adding an Error Image

The error parameter shows a fallback drawable when the request fails. There is also fallback, used specifically when the model is null (no data to load at all).

Setting all three, placeholder, error, and fallback, covers every visual state with almost no code.

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage

@Composable
fun RobustImage(url: String?) {
    AsyncImage(
        model = url,
        contentDescription = null,
        placeholder = painterResource(R.drawable.placeholder_gray),
        error = painterResource(R.drawable.image_broken),
        fallback = painterResource(R.drawable.no_image)
    )
}

Placeholder via ImageRequest

You can also set these states on the ImageRequest itself. This is handy when you build the request once and reuse it, or when you need other request options at the same time.

The request-level functions take drawable resource ids.

import android.content.Context
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.request.error
import coil3.request.placeholder

fun buildRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .crossfade(true)
        .placeholder(R.drawable.placeholder_gray)
        .error(R.drawable.image_broken)
        .build()

Custom UI with SubcomposeAsyncImage

Drawables are fine, but sometimes you want a spinner, shimmer, or text per state. SubcomposeAsyncImage lets you supply real composables for loading, error, and success.

It is more flexible but slightly heavier, so prefer drawable placeholders for long lists and reserve subcompose for hero images.

import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import coil3.compose.SubcomposeAsyncImage

@Composable
fun HeroImage(url: String) {
    SubcomposeAsyncImage(
        model = url,
        contentDescription = "Hero",
        loading = { CircularProgressIndicator() },
        error = { Text("Could not load image") }
    )
}

Reading State Directly

For full control, use the rememberAsyncImagePainter API and inspect its state. The state is a sealed type with Loading, Success, Error, and Empty cases.

This lets you drive your own layout, animations, or analytics based on what happened.

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter

@Composable
fun StateAwareImage(url: String) {
    val painter = rememberAsyncImagePainter(model = url)
    val state = painter.state.collectAsState().value

    when (state) {
        is AsyncImagePainter.State.Loading -> { /* show spinner */ }
        is AsyncImagePainter.State.Error -> { /* show error UI */ }
        else -> Image(painter = painter, contentDescription = null)
    }
}

Showing a Shimmer Placeholder

A popular pattern is a shimmer: an animated gray gradient that suggests content is coming. You can build a simple version with a solid colored box behind the loading state.

Here we use SubcomposeAsyncImage with a tinted box during loading. Swap the box for a shimmer library if you want the animated effect.

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import coil3.compose.SubcomposeAsyncImage

@Composable
fun ShimmerImage(url: String) {
    SubcomposeAsyncImage(
        model = url,
        contentDescription = null,
        loading = {
            androidx.compose.foundation.layout.Box(
                Modifier.fillMaxSize()
                    .background(MaterialTheme.colorScheme.surfaceVariant)
            )
        }
    )
}

Handling a Null or Empty Model

What if you have no URL yet, for example a user without a profile picture? Passing null as the model triggers the fallback drawable (or error if no fallback is set).

This avoids special-casing null in your own code; let Coil show a default avatar.

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage

@Composable
fun ProfilePicture(photoUrl: String?) {
    // If photoUrl is null, Coil shows the fallback default avatar.
    AsyncImage(
        model = photoUrl,
        contentDescription = "Profile picture",
        fallback = painterResource(R.drawable.default_avatar),
        error = painterResource(R.drawable.default_avatar)
    )
}

Listening for Errors with a Listener

Sometimes you need to react in code when a load fails, for example to log it or retry. ImageRequest.Builder accepts a listener with success and error callbacks.

Keep these callbacks light; they run on the main thread.

import android.content.Context
import android.util.Log
import coil3.request.ImageRequest

fun loggedRequest(context: Context, url: String) =
    ImageRequest.Builder(context)
        .data(url)
        .listener(
            onError = { _, result ->
                Log.e("Coil", "Load failed", result.throwable)
            },
            onSuccess = { _, _ ->
                Log.d("Coil", "Loaded $url")
            }
        )
        .build()

Tinting and Color Filters

Placeholder and error drawables often look best when tinted to match your theme. You can apply a ColorFilter to an AsyncImage, which is great for monochrome icon-style fallbacks that should adapt to light and dark mode.

Apply the filter only to the placeholder/error look you want; for real photos you usually leave it off.

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import coil3.compose.AsyncImage

@Composable
fun TintedFallback(url: String?) {
    AsyncImage(
        model = url,
        contentDescription = null,
        error = painterResource(R.drawable.ic_image_placeholder),
        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant)
    )
}

A Resilient Image Component

Let's assemble a reusable component that handles every state cleanly: a loading background, an error fallback, crossfade, and crop. Drop this into a list and never worry about empty holes again.

import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade

@Composable
fun ResilientImage(url: String?, size: Int = 96) {
    val context = LocalContext.current
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(url)
            .crossfade(true)
            .build(),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        placeholder = painterResource(R.drawable.placeholder_gray),
        error = painterResource(R.drawable.image_broken),
        fallback = painterResource(R.drawable.default_avatar),
        modifier = Modifier.size(size.dp)
    )
}

Quick Check

You pass model = null to AsyncImage. Which drawable does Coil show by default?

Recap

You made image loading graceful. Key points:

  • placeholder shows while loading, error on failure, fallback when the model is null.
  • Set them on AsyncImage or on the ImageRequest.
  • SubcomposeAsyncImage lets you supply real composables (spinner, shimmer) per state.
  • rememberAsyncImagePainter exposes the raw state for full control.
  • Use a request listener to log or react to load results.

Next: caching and performance, so images load instantly the second time.

Frequently asked questions

Is the “Placeholders and Error States” lesson free?

Yes — the full text of “Placeholders and Error States” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.

What will I learn in “Placeholders and Error States”?

Graceful loading and failures. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Android Academy?

No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Placeholders and Error States” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Android Academy lesson?

Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Loading Images with Coil
  2. Placeholders and Error States
  3. Caching and Performance
  4. Playing Audio and Video
← Back to Android Academy