Cards

Back in this track's very first lesson, the "Quick Look Ahead" example sketched a simple RecipeCard with just a title and a prep time. Now that we know Scaffold, state-driven dialogs and lists, we can build that card properly: Card is Compose's surface composable for exactly this kind of grouped, elevated content block, and this lesson turns it into the real recipe row RecipeBox's list uses — thumbnail, title, prep time, and a tappable favorite icon.

Card's Core Parameters

ParameterPurpose
shapeCorner shape, e.g. RoundedCornerShape(12.dp) — defaults to Material 3's medium shape
colorsCardDefaults.cardColors(containerColor = ...) to override the background
elevationCardDefaults.cardElevation(defaultElevation = ...) — controls the shadow, giving a "lifted" look
onClickOnly available if you use the dedicated clickable overload, covered next

Plain Card vs. Clickable Card

Card has two overloads: a plain one (no interaction) and one that takes an onClick directly, which automatically adds a ripple and correct accessibility semantics — this is the one to reach for whenever the whole card is tappable, rather than manually chaining Modifier.clickable onto a plain Card:

Card(
    onClick = { onRecipeClick(recipe) },
    shape = RoundedCornerShape(12.dp),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
    modifier = Modifier
        .fillMaxWidth()
        .padding(horizontal = 12.dp, vertical = 6.dp)
) {
    // card content
}

Worked Example: The RecipeBox Recipe Card

The card's body is an ordinary Row — a thumbnail on the left, a title/prep-time Column in the middle taking up remaining space via Modifier.weight(1f), and a favorite toggle on the right:

@Composable
fun RecipeCard(
    recipe: Recipe,
    isFavorite: Boolean,
    onClick: () -> Unit,
    onFavoriteToggle: () -> Unit
) {
    Card(
        onClick = onClick,
        shape = RoundedCornerShape(12.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 12.dp, vertical = 6.dp)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.padding(12.dp)
        ) {
            AsyncImage(
                model = recipe.photoUri,
                contentDescription = recipe.title,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .size(64.dp)
                    .clip(RoundedCornerShape(8.dp))
            )

            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(horizontal = 12.dp)
            ) {
                Text(text = recipe.title, style = MaterialTheme.typography.titleMedium)
                Text(
                    text = "${recipe.prepMinutes} min",
                    style = MaterialTheme.typography.bodySmall
                )
            }

            IconButton(onClick = onFavoriteToggle) {
                Icon(
                    imageVector = if (isFavorite) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
                    contentDescription = "Toggle favorite"
                )
            }
        }
    }
}

AsyncImage comes from Coil, covered in the Images lesson earlier in this track — it handles loading recipe.photoUri off the main thread and caching the result, which matters here since a recipe list can scroll through dozens of thumbnails.

Dropping It Into the List

Back in the LazyColumn from the Scaffold lesson, each item simply becomes a RecipeCard instead of a plain Text row:

items(recipes, key = { it.id }) { recipe ->
    RecipeCard(
        recipe = recipe,
        isFavorite = favoriteIds.contains(recipe.id),
        onClick = { onRecipeClick(recipe) },
        onFavoriteToggle = { onFavoriteToggle(recipe.id) }
    )
}

Notice that RecipeCard itself holds no state — isFavorite is passed in and every event is bubbled up through a callback, the same state-hoisting shape covered earlier in this track. That's what keeps this card reusable across the plain recipe list, search results, and any future favorites screen without duplicating logic.

Note: This is correct, real Compose/Kotlin code — but this website's own project doesn't run a live Android Studio build, so you won't see it actually render on this page.
Next up: Checkbox, RadioButton & Switch covers the selection controls used inside a recipe's ingredient list and its category picker.