Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
4.6 KiB
Lists & Grids
Compose patterns for lazy layouts, applied within MVI architecture.
LazyColumn and LazyRow
Only compose visible items — use for large or dynamic lists. For small fixed lists (<10 items), prefer Column/Row.
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { HeaderSection() }
items(items = users, key = { it.id }) { user ->
UserRow(user = user, onOpen = onOpenUser)
}
item { FooterSection() }
}
DSL patterns
item { }— single composable (header, footer, divider)items(list, key) { }— from a list with stable keysitemsIndexed(list) { index, item -> }— when index is needed
Keys
Always provide stable, unique keys when the list can change.
// GOOD: stable domain ID
items(users, key = { it.id }) { user -> UserRow(user) }
// BAD: index-based — state corrupts on reorder/remove
items(users, key = { index }) { user -> UserRow(user) }
// BAD: no key — Compose can't distinguish items reliably
items(users) { user -> UserRow(user) }
Rule: Use domain IDs, not indices. Without stable keys, removing an item corrupts the state of remaining items.
ContentType for Recycling
Use contentType when rendering different item types to enable layout reuse:
sealed class FeedItem {
data class Header(val title: String) : FeedItem()
data class Post(val id: String, val content: String) : FeedItem()
}
LazyColumn {
items(
items = feedItems,
key = { when (it) { is FeedItem.Header -> it.title; is FeedItem.Post -> it.id } },
contentType = { when (it) { is FeedItem.Header -> "header"; is FeedItem.Post -> "post" } }
) { item ->
when (item) {
is FeedItem.Header -> SectionHeader(item.title)
is FeedItem.Post -> PostCard(item)
}
}
}
Without contentType, all items compete for one reuse pool. With it, items reuse layout state efficiently within their type.
Grids and Pager
LazyVerticalGrid
// Fixed columns
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
items(items, key = { it.id }) { item -> GridItem(item) }
}
// Adaptive columns (responsive) — preferred for responsive layouts
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 120.dp)) {
items(items, key = { it.id }) { item -> GridItem(item) }
}
LazyVerticalStaggeredGrid
For Pinterest-style variable-height layouts:
LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) {
items(images, key = { it.id }) { image -> ImageCard(image) }
}
HorizontalPager / VerticalPager
val pagerState = rememberPagerState(pageCount = { pages.size })
HorizontalPager(state = pagerState) { page ->
PageContent(pages[page])
}
// Programmatic scroll
LaunchedEffect(targetPage) { pagerState.animateScrollToPage(targetPage) }
Scroll State and Derived Logic
val listState = rememberLazyListState()
// GOOD: derivedStateOf for scroll-dependent UI
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 2 }
}
LazyColumn(state = listState) {
items(items, key = { it.id }) { item -> ItemRow(item) }
}
if (showScrollToTop) {
FloatingActionButton(onClick = { scope.launch { listState.animateScrollToItem(0) } }) {
Icon(Icons.Default.ArrowUpward, contentDescription = "Scroll to top")
}
}
Keep LazyListState local — do not put scroll position in the MVI ViewModel state.
Nested Scrolling
// BAD: verticalScroll inside LazyColumn — two scroll containers fight for input
LazyColumn {
item {
Column(Modifier.verticalScroll(rememberScrollState())) { /* conflict */ }
}
}
// OK: nested LazyRow inside LazyColumn (different axes)
LazyColumn {
item { LazyRow { items(horizontalItems) { HorizontalCard(it) } } }
items(verticalItems) { VerticalRow(it) }
}
For complex scenarios, use Modifier.nestedScroll() with a custom NestedScrollConnection.
List Anti-Patterns
| Anti-pattern | Fix |
|---|---|
| No keys on mutable lists | Always provide stable domain ID keys |
| Index-based keys | Use it.id, not position index |
| Expensive computation inside item lambda | Compute upstream in reducer, pass pre-computed data |
Inline filter/sort inside items {} |
Sort/filter in reducer or ViewModel before emitting state |
LazyColumn for 5 fixed items |
Use Column for small fixed lists |
Creating new objects in key lambda |
Use primitive stable identifiers |
Missing contentType on multi-type lists |
Provide contentType for efficient reuse |
For paginated lists with network/database loading, see Paging 3.