State, remember and hoisting
Lesson 2 of 51 min read
Where each piece of state belongs, and the "state down, events up" rule.
Three places state can live, and the question that picks one: who else needs it, and would the user mind if it reset?
remember— survives recomposition, dies on rotation. A dropdown's open flag.rememberSaveable— survives rotation and process death. A text field, a selected tab.- ViewModel — anything from the data layer, or anything that must outlive the composable.
State hoisting
A composable that owns its state cannot be reused or tested. So move state up and pass a value down plus an event up.
// stateless: value in, event out
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
// the caller owns the truth
var query by rememberSaveable { mutableStateOf("") }
SearchBar(query, onQueryChange = { query = it })
This is "state down, events up". It gives you one source of truth, a previewable composable, and a unit-testable owner.
Effects, briefly
LaunchedEffect(key)— run a coroutine when the key changes; cancelled automatically when the composable leaves.DisposableEffect— register something and clean it up on exit (a listener, a sensor).derivedStateOf— compute a value from other state without recomposing on every keystroke.rememberUpdatedKeypatterns — when a long-running effect must see the latest lambda without restarting.
Two traps
// wrong: a new list every recomposition, so nothing is ever skipped
val filtered = items.filter { it.isActive }
// right: recomputed only when the inputs change
val filtered = remember(items) { items.filter { it.isActive } }
The usual mistake: using
rememberfor something the user typed. Rotate the phone and it is gone. If the user produced it, it belongs inrememberSaveableor the ViewModel.
