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>
This commit is contained in:
2026-07-07 16:55:43 +02:00
parent ad914067a9
commit fd4fff2c86
44 changed files with 10509 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
# 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).