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 count | You specify a minimum cell width; column count is derived |
| Simple, predictable, same column count on every device | Naturally scales from phone to tablet to foldable |
| Can look sparse on wide screens, cramped on narrow ones | Cell size stays visually consistent across screen sizes |
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.