mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
162 lines
4.6 KiB
Markdown
162 lines
4.6 KiB
Markdown
# 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`.
|
|
|
|
```kotlin
|
|
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.
|
|
|
|
```kotlin
|
|
// 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:
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
// 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:
|
|
|
|
```kotlin
|
|
LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2)) {
|
|
items(images, key = { it.id }) { image -> ImageCard(image) }
|
|
}
|
|
```
|
|
|
|
### HorizontalPager / VerticalPager
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
// 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](paging.md).
|