Intermediate: Android basics · Jetpack Compose UI · topic 25 of 50 · 30 min
Lists and grids
Explanation
Why lazy lists?
A WhatsApp chat list, a Flipkart search result, a bank statement: lists can have thousands of items. A normal Column with verticalScroll composes and measures every item, even those far off-screen, which wastes memory and makes the screen slow. Lazy layouts compose only the items that are visible (plus a small buffer), and reuse work as you scroll.
data class Txn(val id: Long, val title: String, val amount: Int)
@Composable
fun Statement(txns: List<Txn>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
item { Text("October 2026", style = MaterialTheme.typography.titleMedium) } // a single header
items(txns, key = { it.id }) { txn ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(txn.title)
Text("Rs ${txn.amount}")
}
}
}
}The data for a list should come from state, typically a List exposed by a ViewModel. When that list changes, for example after a new transaction arrives, the LazyColumn recomposes and shows the new items. Keep each row composable small and pass it only the data it needs.
Keys
By default, items are identified by their position. If the list changes (a new message at the top, an item deleted), positions shift and Compose may attach state, like an expanded card or a text field's content, to the wrong item. Giving each item a stable unique key, usually its database id, fixes this and lets Compose animate changes. Keys must be unique within the list.
Clickable items and dividers
items(contacts, key = { it.phone }) { c ->
ListItem(
headlineContent = { Text(c.name) },
supportingContent = { Text(c.phone) },
modifier = Modifier.clickable { onOpen(c) },
)
HorizontalDivider()
}LazyRow
LazyRow works the same way horizontally, for carousels such as "Recently viewed" or category chips. A screen can have a LazyColumn containing LazyRows, as long as each lazy layout scrolls in a different direction.
Grids
@Composable
fun ProductGrid(products: List<String>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 150.dp), // as many columns as fit
contentPadding = PaddingValues(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(products) { name ->
Card { Text(name, Modifier.padding(16.dp)) }
}
}
}Scroll state
rememberLazyListState() gives you the list's scroll state. You can read which item is first visible (to show a "scroll to top" button) and scroll in code with a coroutine: scope.launch { listState.animateScrollToItem(0) }.
Common mistakes
- Using Column with verticalScroll for hundreds of items: slow and memory-hungry. Use LazyColumn.
- Putting a LazyColumn inside a Column with verticalScroll: crash ("Vertically scrollable component was measured with an infinity maximum height constraints"). Make the whole screen one LazyColumn with item { } sections.
- Duplicate keys: the app crashes. Keys must be unique.
- Doing heavy work (like formatting dates for every item) on each recomposition. Prepare the data in the ViewModel.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Lists and grids · 1/6
Long lists need lazy layouts. They compose only the items on screen.
Prefer a video by a teacher? Find free videos on “jetpack compose lazycolumn items key lazyverticalgrid tutorial” on YouTube ↗
In real life: A restaurant menu with categories
A restaurant ordering app shows a long menu: a header for each category (Starters, Main course, Desserts) and dishes under each. One LazyColumn uses item { } for each category header and items(dishes, key = { it.id }) for the dishes. Only visible dishes are composed, so scrolling stays smooth on a budget phone, and stable keys keep each dish's quantity counter attached to the right dish when the menu is filtered.
@Composable
fun Menu(categories: Map<String, List<Dish>>) {
LazyColumn {
categories.forEach { (category, dishes) ->
item(key = category) { Text(category, style = MaterialTheme.typography.titleLarge) }
items(dishes, key = { it.id }) { dish -> DishRow(dish) }
}
}
}Practice (0/3 done)
Why is LazyColumn better than Column(Modifier.verticalScroll(...)) for 5,000 items?
Which grid gives more columns on a tablet than on a phone?
Build a contacts screen: a LazyColumn with a header item, then contacts sorted by name with key = phone, each a clickable ListItem with a divider. Add a "Scroll to top" button that appears when the first visible item index is above 5 and scrolls back with animateScrollToItem.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Text input and formsJetpack Compose UI · same module
- Lists with ListViewFlutter and Dart · Lists, navigation and forms
- Grids with GridViewFlutter and Dart · Lists, navigation and forms
- Positioning and stackingFrontend Web Development · CSS layout and responsive design
- Interview questions: Android and ComposeAndroid career path
- CSS GridFrontend Web Development · CSS layout and responsive design
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does a key in items(list, key = { it.id }) do?
Answer: Identifies each item stably so state and animations follow the right item
Keys survive reordering.
What happens with duplicate keys in a LazyColumn?
Answer: The app crashes
Keys must be unique.
Which is the right way to build a scrolling screen with a header and a long list?
Answer: One LazyColumn with item { header } and items { }
Nested same-direction scrolling crashes.
Which composable shows a horizontal carousel efficiently?
Answer: LazyRow
LazyRow is the horizontal lazy list.
What does lazy building mean for ListView.builder?Related
Answer: Items are built only when they are about to be visible
Saves memory and time.
From Lists with ListView
Which delegate always uses exactly 3 columns?Related
Answer: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3)
Fixed count fixes the columns.
From Grids with GridView
Which value keeps a header visible while you scroll its section, but in the normal flow before that?Related
Answer: sticky
sticky behaves normally until it reaches the given edge, then sticks.
Why must a ViewModel not hold an Activity reference?Related
Answer: The ViewModel outlives the activity, causing a memory leak
The activity is recreated on rotation.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 1 more topic in “Jetpack Compose UI” before its exam: Text input and forms.
- Module 6 exam: Jetpack Compose UI · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Navigation and app architecture. 50 topics left in the course before the final exam.