This episode optimizes lists: LazyColumn, LazyRow, and LazyVerticalGrid, efficient item rendering with key, Paging 3 integration for endless data, and animations in lazy lists.

In episode 4 you were introduced to LazyColumn. Now it's time to use it to the fullest. Lists are the backbone of social, e-commerce, and dashboard applications — and how you render lists determines how smooth the application is.
Lazy lists in Compose only create items that are visible, reuse existing layouts, and discard those far off screen. The key is telling Compose which items are the same via key.
Episode 9 covers basic lazy lists, item reuse and keys, grid layouts, Paging 3 integration, and animations.
LazyColumn renders a vertical list, LazyRow a horizontal one. Both use a DSL block with the item and items functions:
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item { HeaderList() }
items(produk) { produk ->
KartuProduk(produk)
}
}contentPadding adds space from the screen edges, spacedBy(12.dp) adds space between items, and mixing item with items allows a header above the data list. A single LazyColumn can hold many content types.
For catalogs and galleries, LazyVerticalGrid arranges items in columns:
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(foto) { foto ->
KartuFoto(foto)
}
}GridCells.Fixed(2) sets two fixed columns, and GridCells.Adaptive(160.dp) adjusts the number of columns to the screen width. For an adaptive grid, choose Adaptive; for a fixed layout, choose Fixed.
Every lazy list item should be given a stable key. The key tells Compose that an item is the same even when its position shifts — important for preserving item state and animations:
items(items = produk, key = { it.id }) { produk ->
KartuProduk(produk)
}key = { it.id } uses the product ID as the identity. Without a key, Compose uses position as the identity, so item state can stick to the wrong item when the list changes — a detail that also matters for performance in episode 15.
When a list holds different item types, use contentType so Compose reuses layouts correctly:
items(
items = feed,
key = { it.id },
contentType = { it.jenis }
) { item ->
when (item.jenis) {
"teks" -> KartuTeks(item)
"gambar" -> KartuGambar(item)
}
}contentType = { it.jenis } groups items by type so Compose doesn't swap layouts wastefully.
Paging 3 streams pages of data to the UI. collectAsLazyPagingItems turns Flow<PagingData<T>> into an object LazyColumn can consume:
val pagingItems = viewModel.pagingFlow.collectAsLazyPagingItems()
LazyColumn {
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let { artikel ->
KartuArtikel(artikel)
}
}
}pagingItems.itemCount changes as pages load, and pagingItems[index] returns null while an item is still loading — that's where loading placeholders go. Paging 3 automatically handles loading the next page when the user approaches the end of the list.
Watch loadState to show loading and retry states:
when (pagingItems.loadState.refresh) {
is LoadState.Loading -> LoadingIndikator()
is LoadState.Error -> {
Text("Gagal memuat", Modifier.padding(16.dp))
}
else -> {}
}loadState.refresh shows the status of the initial load, and loadState.append shows the status of loading subsequent pages. Both are used to show an indicator below the list while scrolling. To simulate scrolling during manual testing, adb shell input swipe 540 1600 540 400 swipes the screen down on the emulator.
Lazy lists support animation of item insertion and removal:
LazyColumn {
items(items = tugas, key = { it.id }) { tugas ->
Row(
modifier = Modifier
.fillMaxWidth()
.animateItem()
) {
Text(tugas.judul)
}
}
}Modifier.animateItem() animates item movement, insertion, and removal. Deeper animation details are covered in episode 12; what matters here is that a stable key is a prerequisite for list animations to work correctly.
Episode 9 optimized lists: LazyColumn, LazyRow, and LazyVerticalGrid for varied layouts, key and contentType for efficient rendering, Paging 3 integration with collectAsLazyPagingItems and loadState, and item animations with animateItem.
Key takeaways:
key preserves item identity and state.contentType enables layout reuse for similar items.animateItem needs a stable key to work correctly.In episode 10 we will discuss side effects and coroutines — LaunchedEffect, SideEffect, and DisposableEffect, coroutine integration with Compose, loading states and network calls, and lifecycle-aware effects.