Images (painterResource & Coil)
RecipeBox shows photos in two very different situations: small bundled icons (a placeholder chef's
hat, a category badge) that ship inside the app, and user-added recipe photos that live somewhere
out on the network or the device's own storage. Compose's built-in Image composable
handles the first case directly; the second needs a dedicated image-loading library, and the
standard choice for Compose is Coil.
Local Drawables with painterResource
For an image bundled into the app under res/drawable, use the Image
composable together with painterResource, which turns a drawable resource ID into a
Painter Compose can draw:
@Composable
fun RecipePlaceholderIcon(modifier: Modifier = Modifier) {
Image(
painter = painterResource(id = R.drawable.ic_recipe_placeholder),
contentDescription = "No photo added yet",
modifier = modifier.size(96.dp)
)
}
RecipeBox uses this for a recipe that hasn't had a photo attached yet — a simple bundled chef's-hat
icon shown in place of the missing photoUri.
ContentScale: Crop, Fit, FillBounds
contentScale controls how an image is resized to fit the box it's placed in when the
image's own aspect ratio doesn't match that box's aspect ratio:
| ContentScale | Behavior |
|---|---|
ContentScale.Crop | Scales up to fill the entire box, cropping any excess. No letterboxing, but part of the image may be cut off. What RecipeBox uses for photo thumbnails. |
ContentScale.Fit | Scales down to fit entirely within the box, preserving aspect ratio. May leave empty space (letterboxing) on one axis. |
ContentScale.FillBounds | Stretches the image to exactly fill the box, ignoring aspect ratio — can visibly distort the image. |
Remote Photos with Coil's AsyncImage
A RecipeBox recipe's photoUri is usually a URL pointing at an image the user uploaded,
not a local file — Image and painterResource have no idea how to fetch,
decode, and cache that. Coil (via the coil-compose Gradle dependency)
adds an AsyncImage composable that does all three automatically:
// build.gradle.kts (module level)
// implementation("io.coil-kt:coil-compose:2.6.0")
@Composable
fun RecipePhoto(photoUri: String, modifier: Modifier = Modifier) {
AsyncImage(
model = photoUri,
contentDescription = "Recipe photo",
contentScale = ContentScale.Crop,
modifier = modifier.size(120.dp)
)
}
That's the whole integration for the common case — model accepts a plain
String URL (it also accepts a File, Uri, or resource ID),
and Coil handles the network request, in-memory and disk caching, and downsampling to the target
size on a background thread, all without you writing any coroutine code.
Placeholder and Error Handling
A recipe photo takes time to load, and sometimes the URL is broken or the device is offline —
AsyncImage accepts placeholder and error painters to cover
both cases so the UI never shows a blank gap:
@Composable
fun RecipePhoto(
photoUri: String?,
modifier: Modifier = Modifier
) {
AsyncImage(
model = photoUri,
contentDescription = "Recipe photo",
contentScale = ContentScale.Crop,
placeholder = painterResource(id = R.drawable.ic_recipe_placeholder),
error = painterResource(id = R.drawable.ic_recipe_broken_image),
modifier = modifier.size(120.dp)
)
}
placeholder shows immediately while the real photo is still downloading, and
error shows instead of a broken image if the request fails — for example if
photoUri is null (a recipe that never had a photo attached) or the URL
no longer resolves. For more control than static painters give you, Coil also exposes an
onState callback and a SubcomposeAsyncImage variant that lets you show an
arbitrary composable, like a CircularProgressIndicator, while loading is in progress.