LazyVerticalGrid

LazyColumn gives RecipeBox a single-column, recycled list of recipes. But a lot of recipe apps present their collection as a grid of cards instead — two (or more) recipes per row, each showing a photo. LazyVerticalGrid is Compose's lazily-recycled grid layout, and it works on exactly the same recycling principle as LazyColumn: only the cells actually visible get composed.

GridCells.Fixed: A Fixed Number of Columns

The simplest way to configure a grid is to say exactly how many columns it should have, regardless of screen width:

LazyVerticalGrid(
    columns = GridCells.Fixed(2)
) {
    items(recipes) { recipe ->
        RecipeGridCard(recipe)
    }
}

GridCells.Fixed(2) always lays out exactly two columns, splitting the available width evenly between them. That's predictable and simple, but on a tablet or a wide unfolded screen, two columns can end up looking sparse and oversized — the layout doesn't adapt to the space it's given.

GridCells.Adaptive: Columns That Respond to Available Width

GridCells.Adaptive(minSize) takes a different approach: instead of naming a column count, you name the minimum width you want each cell to have, and Compose works out how many columns of at least that width fit in the available space — fitting more columns on a wide screen and fewer on a narrow one:

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 160.dp)
) {
    items(recipes) { recipe ->
        RecipeGridCard(recipe)
    }
}
GridCells.Fixed(n)GridCells.Adaptive(minSize)
You specify an exact column countYou specify a minimum cell width; column count is derived
Simple, predictable, same column count on every deviceNaturally scales from phone to tablet to foldable
Can look sparse on wide screens, cramped on narrow onesCell size stays visually consistent across screen sizes
Rule of thumb: reach for Adaptive for a general-purpose grid like RecipeBox's recipe grid, and reserve Fixed for cases where you deliberately want a specific, unchanging column count (like a 3x3 puzzle board).

Worked Example: RecipeBox's Recipe Grid

The Recipe List screen from the last lesson used a single-column LazyColumn. Here's the same recipe data presented as a two-column grid instead, each cell a card with a photo, title, and prep time:

@Composable
fun RecipeGridScreen(recipes: List<Recipe>) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        contentPadding = PaddingValues(12.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(items = recipes, key = { it.id }) { recipe ->
            RecipeGridCard(recipe = recipe)
        }
    }
}

@Composable
fun RecipeGridCard(recipe: Recipe) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Column {
            AsyncImage(
                model = recipe.photoUri,
                contentDescription = recipe.title,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(120.dp),
                contentScale = ContentScale.Crop
            )
            Column(modifier = Modifier.padding(8.dp)) {
                Text(text = recipe.title, style = MaterialTheme.typography.titleSmall, maxLines = 1)
                Text(text = "${recipe.prepMinutes} min", style = MaterialTheme.typography.bodySmall)
            }
        }
    }
}

Because LazyVerticalGrid recycles cells the same way LazyColumn does, this scales to RecipeBox's entire collection without composing off-screen cards — and the key keeps each card tied to the correct recipe if the list is re-sorted or filtered by category.

itemsIndexed: When a Grid Cell Needs to Know Its Position

Sometimes a grid cell's appearance should depend on where it sits, not just its data — for example, RecipeBox might want to visually highlight every third recipe as "featured." That's what itemsIndexed is for: it hands your item lambda both the index and the item itself:

LazyVerticalGrid(columns = GridCells.Fixed(2)) {
    itemsIndexed(
        items = recipes,
        key = { _, recipe -> recipe.id }
    ) { index, recipe ->
        RecipeGridCard(
            recipe = recipe,
            isFeatured = index % 3 == 0
        )
    }
}

Note that key still ignores the index and uses recipe.id — the index is only used to decide the isFeatured flag passed into the composable, not as the stable identity of the item.

Note: As throughout this track, this is correct, static Kotlin/Compose code — this website's own project doesn't run a live Android Studio build, so it isn't executing on this page.
Next up: Lists and grids only get you within a single screen. Navigation Compose Basics covers moving between RecipeBox's screens — from the recipe list to a recipe's detail view.