What you'll learn
In Phase 1 you met remember and mutableStateOf. Now we turn that into the single most important design pattern in Compose: state hoisting. Getting this right is what separates a tangle of copy-pasted composables from a clean, reusable, testable UI.
By the end you'll be able to:
- Tell a stateful composable from a stateless one
- Hoist state up to the right caller
- Keep a single source of truth for every piece of state
- Apply the "state down, events up" pattern deliberately
Stateful vs stateless
A composable is stateful when it holds state inside itself with remember. It's convenient — you can drop it anywhere — but it's a black box: the caller can't read the value, can't set it, and can't test the composable with a chosen value. Here's a text field that owns its own text:
// STATEFUL: this composable owns and mutates its own state.
@Composable
fun NameField() {
var name by remember { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
)
}A stateless composable holds no state. It receives the value as a parameter and reports changes through a callback. It doesn't decide anything — it just renders what it's given and announces what happened:
// STATELESS: no state inside. Value comes in, events go out.
@Composable
fun NameField(
name: String, // state in
onNameChange: (String) -> Unit, // event out
) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") },
)
}Key idea
Hoisting the state
State hoisting is the act of moving state out of a composable and up to its caller. You replace the internal remember with two parameters — the current value and an onValueChange callback — exactly as we did above. The caller then owns the state and passes it back down:
// The caller now owns the state — the single source of truth.
@Composable
fun SignupScreen() {
var name by remember { mutableStateOf("") }
Column {
NameField(name = name, onNameChange = { name = it })
Text("Hello, ${name.ifBlank { "stranger" }}!") // reuses the same state
}
}Notice the payoff on the last line: because name now lives in the caller, the greeting Text can read the same value the field is editing. Two composables, one piece of state, always in sync — impossible when the state was locked inside NameField.
Single source of truth
The rule that keeps hoisting sane: every piece of state has exactly one owner — one place where it is declared and mutated. Everyone else receives a read-only copy and asks the owner to change it via a callback. If two composables each kept their own copy of "name," they would drift apart; with one source of truth they cannot.
Where should that owner be? Hoist state to the lowest common ancestor that:
- needs to read the state, and
- needs to change the state, and
- is the common parent of everything that uses it.
Tip
State down, events up
This is the phrase to memorise. Data flows down as parameters; events flow up as callbacks. A child never reaches up to mutate a parent's state directly — it reports "the user typed this" and lets the owner decide what to do. This one-way flow is called unidirectional data flow, and it makes your UI predictable: to understand any screen, you only have to know its state and how events change it.
| Direction | What flows | How |
|---|---|---|
| Down ↓ | State (the current value) | value: String |
| Up ↑ | Events (something happened) | onValueChange: (String) -> Unit |
When not to hoist
Hoisting isn't always right. If a piece of state is a private, internal detail that no parent and no sibling needs — the scroll position of a list, whether a dropdown is open, an animation's progress — keep it local with remember. Hoisting it would only add noise. The test is simple: does anyone else need to read or control this value? If no, keep it inside; if yes, hoist it.
Note
ViewModel — but the pattern is identical.Recap & quick check
Key takeaways
- A stateful composable holds its own state; a stateless one receives value and reports changes via a callback.
- State hoisting moves state up to the caller — replacing an internal remember with value + onValueChange.
- Keep a single source of truth: each piece of state has exactly one owner; everyone else gets a read-only copy.
- State down, events up = unidirectional data flow, which makes the UI predictable.
- Don't hoist purely-internal state (scroll position, an open/closed toggle) — keep it local with remember.
Quick check
1. What is a stateless composable?
2. State hoisting means…
3. Why keep a single source of truth?
4. 'State down, events up' describes…
5. Which state is best kept LOCAL rather than hoisted?
You now know the pattern every Compose app is built on. Next we'll put it to work capturing real user input — buttons, text fields, switches, and a validated form.