Files
vnidrop/.codex/skills/compose-skill/references/lists-grids.md
Hammed Abass fd4fff2c86 docs: add compose development guidelines and skills
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory.

Co-authored-by: Junie <junie@jetbrains.com>
2026-07-07 16:55:43 +02:00

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 keys
  • itemsIndexed(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.