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:

ContentScaleBehavior
ContentScale.CropScales 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.FitScales down to fit entirely within the box, preserving aspect ratio. May leave empty space (letterboxing) on one axis.
ContentScale.FillBoundsStretches 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.

Next up: Buttons & Clickable covers making things tappable — including turning this exact recipe photo into part of a whole clickable recipe card.