# 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).
