LazyColumn and LazyRow
LazyColumn and LazyRow are Compose's equivalents of RecyclerView: scrollable lists that only compose and lay out the items currently visible on screen (plus a small buffer), rather than eagerly rendering every item up front. This 'lazy' composition is what makes it feasible to display lists of thousands of items without the memory and CPU cost of building thousands of composables simultaneously. Unlike a plain Column wrapped in Modifier.verticalScroll, which does render every child regardless of visibility, LazyColumn's items only enter composition as they scroll into the viewport and leave composition (freeing their resources) as they scroll out.
Cricket analogy: LazyColumn only composing visible items is like a stadium only setting up seating in the sections currently filling with spectators, rather than building out every stand for a match that will never fill it.
The Lazy DSL: items, itemsIndexed, and item
Rather than taking a content lambda directly like Column does, LazyColumn takes a content: LazyListScope.() -> Unit lambda, which exposes a small DSL: item { } for a single fixed piece of content (like a header), and items(list) { element -> } for repeating content driven by a collection. itemsIndexed additionally supplies the index alongside each element, useful for numbering or alternating row styles. Because this DSL builds a description of content rather than composing everything immediately, Compose can decide at scroll time which item lambdas actually need to run.
Cricket analogy: The LazyListScope DSL with item{} for a header and items(list){} for repeating content is like a scorecard template with a fixed 'Match Details' header block and a repeating row generator for each of the eleven batsmen.
@Composable
fun ChatMessageList(messages: List<ChatMessage>, currentUserId: String) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
reverseLayout = true,
contentPadding = PaddingValues(vertical = 8.dp, horizontal = 12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
item {
Text(
text = "Start of conversation",
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.fillMaxWidth().padding(8.dp)
)
}
items(
items = messages,
key = { message -> message.id }
) { message ->
MessageBubble(
message = message,
isOwnMessage = message.senderId == currentUserId
)
}
}
}LazyRow and Horizontal Scrolling
LazyRow mirrors LazyColumn exactly but scrolls horizontally, which is the standard building block for carousels, category chip rows, and horizontally scrolling image galleries. Nesting a LazyRow inside a LazyColumn item is fully supported and common — for example, a Netflix-style browsing screen is typically a LazyColumn of section headers, each followed by a horizontal LazyRow of items — but nesting a scrollable Lazy list inside another scrollable container in the same direction (like a LazyColumn inside a verticalScroll Column) causes both infinite-height measurement errors and severely degraded performance, and should be avoided.
Cricket analogy: A LazyRow of category chips inside a LazyColumn is like a broadcast's scrollable format-selector strip (T20, ODI, Test) sitting above the vertically scrolling list of today's matches for that format.
A LazyColumn is conceptually closer to a RecyclerView with a LinearLayoutManager than to a plain Column — both recycle offscreen content, both accept a stable key for each item, and both require careful handling of item identity to animate insertions/removals correctly.
Omitting the key parameter in items() means Compose falls back to using the item's position as its implicit identity. If the underlying list is reordered, filtered, or has items inserted in the middle, Compose can misattribute internal state (like a per-item remembered checkbox state, or in-flight animations) to the wrong data because positions shifted but keys did not exist to track true identity across the change.
- LazyColumn and LazyRow only compose and lay out currently visible items (plus a small buffer), unlike a scrollable plain Column which renders everything eagerly.
- The content lambda uses a LazyListScope DSL with item{}, items(list){} and itemsIndexed(list){} builder functions rather than direct composable calls.
- Providing a stable
keyfor each item lets Compose correctly track item identity across insertions, removals, and reordering. - LazyRow behaves identically to LazyColumn but scrolls horizontally, commonly nested inside LazyColumn items for carousel-style UIs.
- Nesting same-direction scrollable containers (e.g. LazyColumn inside a vertically scrolling Column) causes measurement errors and must be avoided.
- rememberLazyListState exposes scroll position information like firstVisibleItemIndex, useful for scroll-driven UI like a 'scroll to top' button.
Practice what you learned
1. What is the key architectural difference between LazyColumn and a Column wrapped in Modifier.verticalScroll?
2. What does supplying a `key` to the items() function inside LazyColumn accomplish?
3. Why is nesting a LazyColumn inside a Column with Modifier.verticalScroll problematic?
4. What is a common, well-supported pattern for combining LazyRow and LazyColumn?
5. What can rememberLazyListState be used for?
Was this page helpful?
You May Also Like
LazyGrid and Item Keys
Explore LazyVerticalGrid and LazyHorizontalGrid for building grid layouts in Compose, and why stable item keys are essential for correctness.
List Performance and Recomposition
Learn how Compose decides which list items to redraw, and how keys, stable types, and lambdas keep LazyColumn scrolling smoothly under real-world data loads.
mutableStateOf and Recomposition
Dive into how mutableStateOf creates observable state that triggers recomposition, and how Compose determines which composables need to redraw.
Layouts: Row, Column, and Box
Row, Column, and Box are Compose's three foundational layout composables for arranging children horizontally, vertically, or stacked, forming the basis of nearly every screen.