State & Reactivity
A var is state. A $: in front of one makes it derive from other
state. There is no third thing to learn.
Local state
Declare a var in a component's script and it becomes Compose state:
var count = 0
<Button onClick={() => count++}>
<Text>{count}</Text>
</Button> compiles to
var count by remember { mutableStateOf(0) } Reading count in markup subscribes to it, and assigning to it recomposes whatever
read it. You never write remember, mutableStateOf, or .value yourself.
When state outlives the composable
A screen that only holds a couple of fields keeps them in remember, which is lost
on rotation. Past a certain complexity Whitehall promotes the whole script into a ViewModel
instead, and the state survives configuration changes. That happens when a component has
mutable state and any of:
- a
suspendfunction - a lifecycle hook —
$onMountor$onDispose - three or more functions, which is taken as a sign of real state logic
var count = 0
suspend fun loadData() { … } // this makes it a ViewModel
$onMount { loadData() } | Pattern | Survives rotation | Use for |
|---|---|---|
var x = 0, simple script | No | Small forms, toggles |
var x = 0, promoted | Yes | suspend / lifecycle / 3+ functions |
class X { var … } | Yes | Screen state you name |
@store object X | Yes | App-wide, lives the whole process |
Derived values
Put $: in front of a declaration and it recomputes whenever anything it reads
changes:
var count = 0
var multiplier = 2
$: var doubled = count * 2 // tracks count
$: var scaled = count * multiplier // tracks count and multiplier compiles to
val doubled by remember { derivedStateOf { count * 2 } } Dependencies are whatever the expression reads — there is no list to declare and no list to keep in sync when you edit the expression.
val x = expr runs once, and
quietly changing that for some declarations and not others would make the language guess. $: says "this one re-runs" out loud, so a plain val keeps meaning
exactly what it means in Kotlin.See it happen
The source is editable, and the Kotlin is compiled here in your browser by the same compiler the CLI uses — so it is what you would actually get, not a transcription of it.
var count = 0
val doubled = count * 2
<Column gap={16}>
<Text>Count: {count}</Text>
<Text>Doubled: {doubled}</Text>
<Button onClick={() => count++}>Increment</Button>
</Column>A plain val is computed once, at first composition. Nothing in the Kotlin watches count, so doubled keeps whatever value it had when the screen was built.
Effects
A $: block runs its body whenever the state it reads changes:
var count = 0
$: {
println("Count changed to: $count")
analytics.track("count", count)
} compiles to
LaunchedEffect(Unit) {
snapshotFlow {
println("Count changed to: $count")
}.collect {}
} Use it for the things a value can't express: logging, analytics, syncing to disk. If you are
computing something, reach for $: var instead — a derived value only recomputes
when read, while an effect runs whether anyone is looking or not.
$: block makes it re-trigger itself. Derive
instead of assigning, or move the write behind an event handler.Collections
Bracket literals build lists, and mutability follows val versus var:
val numbers = [1, 2, 3] // listOf(1, 2, 3)
var queue = [10, 20] // mutableStateOf(mutableListOf(10, 20)) A var list is state, so pushing to it recomposes the list that renders it. See @for for iterating one.
Ranges
val simple = 1..10 // (1..10).toList()
val stepped = 0..100:2 // step 2
val down = 10..1:-1 // downTo Explicit derivedStateOf
The Compose primitive is still available when you want it, which is worth knowing for an expression that is expensive enough to think about:
var items = []
val sorted = derivedStateOf { items.sortedBy { it.name } } Prefer $: var sorted = … for anything ordinary.
See Also
- @store — state that lives as long as the app
- Data classes — shaping the state you hold
- bind: — wiring state to inputs
- $onMount / $onDispose — running code on entry and exit