Lists with LazyColumn & Grids
Why “Lazy”?
A regular Column builds all its children at once — fine for a few items, disastrous for thousands. LazyColumn and LazyRow only compose the items currently visible, recycling as you scroll. They replace the old RecyclerView + Adapter + ViewHolder boilerplate with a few lines.
A basic list
@Composable
fun UserList(users: List<User>) {
LazyColumn(
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(users, key = { it.id }) { user ->
ListItem(
headlineContent = { Text(user.name) },
supportingContent = { Text(user.email) }
)
}
}
}
Always pass a key
The key = { it.id } gives each item a stable identity so Compose can recycle and animate correctly when the list changes (insertions, deletions, reorders). Without it, updates can be wrong or janky.
Headers, sections and mixed content
LazyColumn {
item { Text("Featured", style = MaterialTheme.typography.titleLarge) }
items(featured) { FeatureCard(it) }
item { Text("All users") }
items(users, key = { it.id }) { UserRow(it) }
}
Grids
LazyVerticalGrid(
columns = GridCells.Fixed(2), // or GridCells.Adaptive(120.dp)
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(photos, key = { it.id }) { photo -> PhotoCard(photo) }
}
Reacting to scroll
val state = rememberLazyListState()
LazyColumn(state = state) { /* ... */ }
// e.g. show a "scroll to top" button when state.firstVisibleItemIndex > 0
Common mistakes
- Forgetting the
key— causes wrong recycling and lost scroll/animation. - Putting a
LazyColumninside a scrollingColumn(infinite height error) — let the LazyColumn do the scrolling. - Loading images synchronously — use Coil’s
AsyncImage.
Summary: UseLazyColumn/LazyRow/LazyVerticalGridfor scrolling content, always pass a stablekey, and mixitemanditemsfor headers and sections.