Kotlin Compose Image Fallback for Coil and Glide
Implement a compose image placeholder in Jetpack Compose using Coil's AsyncImage and Glide Compose with error drawables and deterministic fallback URLs.
Jetpack Compose does not ship a built-in network image component. Coil and Glide fill that gap, each providing placeholder and error drawable APIs that wire directly into the compose image placeholder pattern. Pairing these with deterministic fallback URLs from fallback.pics means every broken or missing image resolves to a branded, dimension-matched placeholder.
This guide covers Coil's AsyncImage model and Glide Compose's GlideImage API, the placeholder/error composable slots, Painter-based fallbacks, and how to use fallback.pics URLs as the error drawable source.
Background
Why Compose needs explicit compose image placeholder handling
Compose renders UI from state. When a network image URL fails, the composable has no built-in mechanism to show an error UI — it simply renders nothing or the last drawn state. Without explicit placeholder and error handling, a product grid shows empty boxes and a user profile screen shows no avatar.
Coil and Glide both provide placeholder/error slots in their Compose DSLs. These accept Painter instances, Drawable references, or composable lambdas. Using a fallback.pics URL as the error source gives you a predictable visual fallback that is dimensionally matched to the composable's size.
Coil
Coil AsyncImage with placeholder and error Painters
Coil's AsyncImage composable takes placeholder and error parameters as Painter or ImageRequest. Use rememberAsyncImagePainter for the error case to load a fallback.pics URL, or pass a local drawable for offline resilience.
Set contentScale and modifier size before the image request resolves. Coil preserves the composable's layout bounds regardless of whether the placeholder, image, or error state is shown.
// build.gradle.kts
// implementation("io.coil-kt:coil-compose:2.6.0")
import coil.compose.AsyncImage
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
@Composable
fun FallbackImage(
url: String?,
contentDescription: String?,
modifier: Modifier = Modifier,
width: Int = 400,
height: Int = 300,
) {
val fallbackUrl = "https://fallback.pics/api/v1/${width}x${height}/7C3AED/FFFFFF?text=No+Image"
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
placeholder = rememberAsyncImagePainter(
model = "https://fallback.pics/api/v1/animated/skeleton/${width}x${height}"
),
error = rememberAsyncImagePainter(model = fallbackUrl),
modifier = modifier.size(width.dp, height.dp)
)
} Glide
GlideImage error and loading composables in Glide Compose
Glide Compose's GlideImage provides loading and failure composable slots that accept arbitrary Compose content. This lets you show a ShimmerBox during load and a full Compose fallback layout on error — more flexible than a static drawable.
Glide's RequestBuilder.error() also accepts a resource ID or URL string for non-Compose failure paths. Use whichever fits your existing Glide configuration.
// implementation("com.github.bumptech.glide:compose:1.0.0-beta01")
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun GlideFallbackImage(
url: String?,
modifier: Modifier = Modifier,
) {
GlideImage(
model = url,
contentDescription = null,
modifier = modifier,
loading = placeholder {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFE5E7EB))
.shimmerEffect()
)
},
failure = placeholder {
AsyncImage(
model = "https://fallback.pics/api/v1/400x300/7C3AED/FFFFFF?text=Unavailable",
contentDescription = "Image unavailable",
modifier = Modifier.fillMaxSize()
)
}
)
} Loop guard
Preventing fallback URL error loops
If the fallback.pics URL itself fails (rare, but possible in offline scenarios), Coil will fire another error event. Guard against this by using a local drawable as the terminal fallback — a vector drawable that ships with the APK never triggers a network request.
Coil's ImageRequest.Builder supports .fallback() for the case where the data is null and .error() for load failures. Use a local drawable resource for .error() and reserve the fallback.pics URL for the .placeholder() so it streams in during load, not as the error recovery.
ImageRequest.Builder(context)
.data(url)
.placeholder(R.drawable.img_placeholder_shimmer)
.error(R.drawable.img_error_local) // local drawable, never 404s
.fallback(R.drawable.img_error_local) // shown when data == null
.build() Performance
Disk cache and memory cache configuration for placeholders
Coil caches images in memory (LruCache) and on disk (DiskLruCache) by default. Fallback.pics URLs return immutable cache headers, so successful fetches land in the disk cache and survive process restarts. Set diskCachePolicy to ENABLED for the error painter to avoid re-fetching the fallback on every list scroll.
Cap the memory cache at 25% of the available heap for image-heavy screens. Product grids with 50+ images can exhaust the default cache size quickly, causing repeated decodes and jank.
Accessibility
Content descriptions across placeholder and error states
Set contentDescription on both the loading state and error state composables. Talkback reads the content description from the innermost focusable element. An empty string silences the announcement for decorative placeholders; a descriptive string like 'Product image unavailable' informs the user on error.
Use LocalInspectionMode.current to detect preview mode and return a static painter instead of making a network request during Compose previews.
Resources
Fallback.pics routes for Android dimensions
Common Android image sizes map directly to fallback.pics API routes. Use these for error drawables or placeholder URLs.
// Common Android fallback URLs
// Product tile (Material card 2:1)
https://fallback.pics/api/v1/400x200/7C3AED/FFFFFF?text=Product
// Avatar (circular, 56dp standard)
https://fallback.pics/api/v1/avatar/56?text=AB
// Full-width hero (360dp typical phone width)
https://fallback.pics/api/v1/360x200/18181B/FFFFFF?text=Hero
// Thumbnail (56x56 list item)
https://fallback.pics/api/v1/56x56/E5E7EB/71717A
// Related posts
https://fallback.pics/blog/react-native-image-fallback/
https://fallback.pics/blog/flutter-image-placeholder-errorbuilder/
https://fallback.pics/docs/ Key takeaways
What to standardize before shipping
- Coil's AsyncImage accepts placeholder and error Painter instances; use rememberAsyncImagePainter with a fallback.pics URL for the error case.
- Glide Compose's failure composable slot accepts arbitrary Compose content, letting you show a branded fallback layout rather than a static drawable.
- Always use a local vector drawable as the terminal error fallback to guard against offline scenarios where even the fallback URL is unreachable.
- Set diskCachePolicy to ENABLED for fallback painters so repeated list scrolls don't trigger redundant network requests.
- Add explicit content descriptions to every image state — empty string for decorative placeholders, descriptive text for error states.
Production fallback layer
Use fallback.pics anywhere an image URL is accepted.
Start with one deterministic URL and standardize fallback behavior across your design system.