Phase 2 · Building Interactive UIsModule 11~30 min read

Lists with LazyColumn & LazyRow

Display long, scrolling lists efficiently with lazy composables — the right way to show data.

What you'll learn

Almost every app shows a list — chats, contacts, a feed, search results. Rendering those efficiently is a solved problem in Compose, but only if you reach for the right tool. This module is about the lazy list composables and the one detail (keys) that keeps them fast and correct.

By the end you'll be able to:

  • Explain why LazyColumn beats a scrolling Column for real data
  • Build vertical and horizontal lists
  • Use item, items, and itemsIndexed
  • Add keys for correctness and performance, and build grids

Why lazy lists

A regular Column composes all its children immediately, whether they're on screen or not. For ten items that's fine; for a thousand it's a disaster — slow startup and wasted memory:

Dont.kt
// DON'T do this for long or dynamic lists:
Column(Modifier.verticalScroll(rememberScrollState())) {
    items.forEach { item ->
        ItemRow(item)   // every item is composed up front — slow, memory-hungry
    }
}

A lazy list only composes the items that are currently visible (plus a small buffer), and recycles them as you scroll — the same idea as RecyclerView, but with almost no boilerplate. Use a scrolling Column only for a handful of known items; reach for LazyColumn for anything long or data-driven.

LazyColumn & LazyRow

LazyColumn scrolls vertically and LazyRow horizontally. Instead of writing children directly, you describe content inside a special scope using items:

ContactList.kt
LazyColumn {
    items(contacts) { contact ->
        ContactRow(contact)          // composed only when scrolled into view
    }
}
9:41
AAda Lovelace
AAlan Turing
GGrace Hopper
KKatherine Johnson
LLinus Torvalds
LazyColumn — how this renders on a device

Tip

Add breathing room with contentPadding = PaddingValues(16.dp) and space items with verticalArrangement = Arrangement.spacedBy(8.dp) — both are parameters on the lazy list itself, so you don't pad each row by hand.

item, items & itemsIndexed

Inside a lazy list you have three building blocks. Use item { } for a single one-off (a header), items(list) to emit one composable per element, and itemsIndexed(list) when you also need each element's position:

Slots.kt
LazyColumn {
    item {                                   // a single, one-off item
        Text("Contacts", style = MaterialTheme.typography.headlineSmall)
    }
    items(contacts) { contact ->             // many items from a list
        ContactRow(contact)
    }
    itemsIndexed(contacts) { index, contact ->   // when you need the position
        Text("${index + 1}. ${contact.name}")
    }
}

Keys — the detail that matters

By default Compose identifies items by their position in the list. That breaks down when the list changes — insert an item at the top and every row shifts by one, so Compose thinks every row changed and needlessly recomposes them (and any per-item state, like an expanded card, jumps to the wrong row). Give each item a stable, unique key and Compose can track items by identity across changes:

Keys.kt
LazyColumn {
    items(
        items = contacts,
        key = { contact -> contact.id },   // stable, unique identity per item
    ) { contact ->
        ContactRow(contact)
    }
}

Key idea

Always provide a key for lists that can change — reorder, insert, delete, or filter. Use something truly unique and stable, like a database id. Never use the list index as the key; that defeats the entire purpose.

Grids

For a gallery-style grid, use LazyVerticalGrid (or LazyHorizontalGrid). You choose the columns with GridCells.Fixed(n) for a set count, or GridCells.Adaptive(minSize) to let Compose fit as many columns as will fit the width — great for responsive layouts across phone and tablet:

PhotoGrid.kt
LazyVerticalGrid(
    columns = GridCells.Fixed(2),          // or GridCells.Adaptive(minSize = 120.dp)
    contentPadding = PaddingValues(12.dp),
    horizontalArrangement = Arrangement.spacedBy(12.dp),
    verticalArrangement = Arrangement.spacedBy(12.dp),
) {
    items(photos) { photo -> PhotoCard(photo) }
}
9:41
LazyVerticalGrid — how this renders on a device

Recap & quick check

Key takeaways

  • LazyColumn/LazyRow compose only visible items and recycle them — use them for any long or dynamic list.
  • Describe content with item {} for one-offs and items(list) / itemsIndexed(list) for collections.
  • A scrolling Column is fine only for a small, fixed number of items.
  • Provide a stable, unique key for changeable lists to avoid needless recomposition and misplaced state.
  • LazyVerticalGrid with GridCells.Fixed(n) or Adaptive(minSize) builds responsive grids.

Quick check

1. Why prefer LazyColumn over a scrolling Column for a 1,000-item list?

2. Which emits one composable per element of a list?

3. You need each row's position number. Use…

4. What should you pass as a list item's key?

5. How do you build a responsive grid that fits as many columns as possible?

Your lists are fast and correct. Now let's make everything beautiful and consistent with Material Design 3.