← Jetpack Compose from Zero to Production

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.
  • rememberUpdatedKey patterns — 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 remember for something the user typed. Rotate the phone and it is gone. If the user produced it, it belongs in rememberSaveable or the ViewModel.