# Compose runtime patterns

Loaded on demand from `../SKILL.md`.

## Who owns a piece of state

Decide ownership before writing the composable. The rule of thumb: state lives
in the lowest common parent of everything that reads or writes it, and no lower.

| Kind of state | Owner | Survives |
|---|---|---|
| Screen data from a repository | ViewModel, exposed as `StateFlow` | configuration change |
| User input the ViewModel must validate | ViewModel | configuration change |
| Pure UI element state (expanded, scroll) | composable via `remember` / `rememberSaveable` | depends on the API |
| Shared by a group of UI elements | a plain state holder class, remembered | same as `remember` |

Collect ViewModel state with the lifecycle-aware collector so collection stops
when the screen is not visible:

```kotlin
@Composable
fun OrderListRoute(viewModel: OrderListViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()
    OrderListScreen(
        state = state,
        onRefresh = viewModel::refresh,
        onOrderClick = viewModel::openOrder,
    )
}
```

`OrderListScreen` takes only values and lambdas. That split keeps the screen
previewable and testable without a ViewModel, and it stops the ViewModel from
leaking into child composables.

### remember versus rememberSaveable

- `remember` keeps a value across recompositions; it is lost when the
  composable leaves the composition or the activity is recreated.
- `rememberSaveable` also writes to the saved-instance bundle, so it survives
  rotation and process death. Use it for text field contents, selected tab,
  expanded flags. Custom types need a `Saver` or `@Parcelize`.
- Pass keys when the remembered value depends on an input:
  `remember(noteId) { NoteDraft(noteId) }` rebuilds when `noteId` changes.

### A plain state holder

When several related UI values move together, group them in a class rather
than scattering `mutableStateOf` calls:

```kotlin
@Stable
class FilterBarState(initial: Set<Category>) {
    var selected by mutableStateOf(initial)
        private set
    var isExpanded by mutableStateOf(false)
        private set

    fun toggle(category: Category) {
        selected = if (category in selected) selected - category else selected + category
    }
    fun expand(value: Boolean) { isExpanded = value }
}

@Composable
fun rememberFilterBarState(initial: Set<Category> = emptySet()) =
    remember { FilterBarState(initial) }
```

## Side effects

The body of a composable must be free of side effects because it can run many
times, out of order, or be discarded. Every effect goes through one of these:

| Need | API | Restarts when |
|---|---|---|
| Run a suspend block tied to the composable | `LaunchedEffect(key)` | key changes |
| Register and unregister a listener | `DisposableEffect(key)` with `onDispose` | key changes |
| Launch from a click handler | `rememberCoroutineScope()` | never; scope dies with the composable |
| Publish Compose state to non-Compose code after every successful recomposition | `SideEffect` | every recomposition |
| Turn a callback-based source into state | `produceState` | key changes |
| Observe state as a Flow inside an effect | `snapshotFlow { ... }` | emits on change |
| Use the latest lambda inside a long effect without restarting it | `rememberUpdatedState(lambda)` | never |

```kotlin
@Composable
fun AutoSaveDraft(draft: String, onSave: suspend (String) -> Unit) {
    val currentDraft by rememberUpdatedState(draft)
    val latestOnSave by rememberUpdatedState(onSave)
    LaunchedEffect(Unit) {
        snapshotFlow { currentDraft }
            .debounce(500)
            .distinctUntilChanged()
            .collect { latestOnSave(it) }
    }
}
```

`snapshotFlow` only sees changes to snapshot state it reads, so the plain
`draft` parameter is wrapped in `rememberUpdatedState` first; reading the bare
parameter would capture its first value and never emit again.

Choosing keys: use the identity the effect depends on (`LaunchedEffect(orderId)`),
never `true` to mean "once" if the dependency can change, and never a fresh
object allocated in the body, which restarts the effect every recomposition.

One-shot events from a ViewModel (show a snackbar, navigate) are collected in a
`LaunchedEffect` with `repeatOnLifecycle` semantics, or modelled as state the UI
consumes and acknowledges. `android-mvi-viewmodel` covers the channel approach.

## Modifiers

Order matters because each modifier wraps the ones after it.

```kotlin
Modifier
    .padding(16.dp)          // outer spacing, not clickable
    .clip(RoundedCornerShape(12.dp))
    .background(MaterialTheme.colorScheme.surfaceVariant)
    .clickable(onClick = onClick)  // ripple is clipped to the shape
    .padding(12.dp)          // inner spacing, inside the touch target
```

- Every public composable takes `modifier: Modifier = Modifier` as its first
  optional parameter and applies it to the root, once.
- Write custom modifiers with `Modifier.Node` and a `ModifierNodeElement`;
  `Modifier.composed` is discouraged because it allocates per call site and
  cannot skip.

```kotlin
private class DimWhenNode(var dimmed: Boolean) : Modifier.Node(), DrawModifierNode {
    override fun ContentDrawScope.draw() {
        drawContent()
        if (dimmed) drawRect(Color.Black.copy(alpha = 0.4f))
    }
}

private data class DimWhenElement(val dimmed: Boolean) : ModifierNodeElement<DimWhenNode>() {
    override fun create() = DimWhenNode(dimmed)
    override fun update(node: DimWhenNode) {
        node.dimmed = dimmed
        node.invalidateDraw()
    }
}

fun Modifier.dimWhen(dimmed: Boolean) = this then DimWhenElement(dimmed)
```
