On this page
A new Compose developer builds a feed with Column(Modifier.verticalScroll(...)) and forEach. It works with 20 items. With 2,000 it takes seconds to open and stutters while scrolling. The fix is one word, Lazy, plus a few details that matter more than they look.
The core difference
| Aspect | Column | LazyColumn |
|---|---|---|
| Composes | All children immediately | Visible items (plus prefetch) |
| Scrolling | Needs Modifier.verticalScroll | Built in |
| Good for | Small, fixed content: forms, detail pages, settings sections | Long or unbounded lists: feeds, search results, chats |
| API | Normal composable children | DSL: item, items, stickyHeader |
| Scroll state | rememberScrollState() | rememberLazyListState() (index-based) |
LazyColumn done right
@Composable
fun ChatList(messages: List<ChatItem>, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier,
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = messages,
key = { it.id }, // stable identity: correct reuse, animations, state
contentType = { it::class }, // reuse compositions between items of the same type
) { item ->
when (item) {
is ChatItem.DateDivider -> DateDivider(item.date)
is ChatItem.Message -> MessageBubble(item, Modifier.animateItem()) // animate inserts/moves
}
}
}
}key: without stable keys, inserting at the top shifts item state (such as an expanded card) to the wrong rows, and scroll position jumps. Use your item's ID.contentType: tells Compose which items share structure, so it can reuse work efficiently in mixed lists.Modifier.animateItem()animates insertions, removals and reordering.
When Column is the better choice
@Composable
fun ProductDetails(product: Product) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp)) {
ProductImage(product.imageUrl)
Text(product.name, style = MaterialTheme.typography.headlineSmall)
PriceTag(product.pricePaise)
Text(product.description)
SpecsTable(product.specs) // a handful of rows
}
}With a small, bounded number of children, a Column is simpler, and laziness adds overhead without benefit.
The nested scrolling trap
Putting a LazyColumn inside a Column with verticalScroll throws an error, because a lazy list can't be measured with infinite height. Don't nest them. Make the whole screen one LazyColumn and put the header content in item { } blocks:
LazyColumn {
item { ProfileHeader(user) }
item { StatsRow(user.stats) }
stickyHeader { SectionTitle("Posts") }
items(posts, key = { it.id }) { PostCard(it) }
if (isLoadingMore) item { LoadingRow() }
}Horizontal lists inside a vertical LazyColumn (a LazyRow per section, like a store front) are fine and common.
Scroll state and actions
val shouldLoadMore by remember {
derivedStateOf {
val last = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
last >= listState.layoutInfo.totalItemsCount - 5
}
}
LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) onLoadMore() }For real paging from APIs or databases, prefer Paging 3, which handles loading states, retries and caching.
Performance tips
- Keep item composables cheap. Precompute formatted strings in the ViewModel.
- Load images at the item's display size with an image library.
- Avoid reading
listStatedirectly in the item builder. Wrap derived values inderivedStateOf. - Judge scrolling smoothness on release builds with Baseline Profiles. See Compose Performance Optimization.
Choosing in practice
Settings screen (15 rows)
Either works. A Column is simpler. Choose LazyColumn if the rows come from data and may grow.
Product grid
LazyVerticalGrid or LazyVerticalStaggeredGrid with keys.
Chat
LazyColumn(reverseLayout = true) keeps the newest messages at the bottom, anchored as new ones arrive.
Frequently asked questions
Is LazyColumn the same as RecyclerView?
Conceptually similar: both render only what's visible. LazyColumn needs no adapter or ViewHolder, and diffing is driven by keys.
Why does my item's remembered state reset when scrolling?
Items leaving the screen are disposed. Hoist important state into the item model or ViewModel, or use rememberSaveable with stable keys.
Can I use LazyColumn inside a Scaffold?
Yes. Pass the Scaffold's padding values to contentPadding so items aren't hidden behind bars.
Conclusion
Use Column for small, fixed content and LazyColumn for lists that can grow. Give lazy items stable keys and contentType, never nest a lazy list inside a scrolling Column, and keep item composables cheap.
Next step: bring your lists to life with Jetpack Compose Animations for Beginners.
Official references
π± Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it βΈ
NotiVaultNotification history: recover dismissed notificationsGet it βΈ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.