State is what makes UI interactive. Learn remember and mutableStateOf to hold state across recompositions, why state hoisting makes composables reusable and testable, and rememberSaveable for surviving rotation.
A composable re-runs on every recomposition, so a plain variable resets each time. To hold state you wrap it in mutableStateOf (which Compose observes) and remember (which preserves it across recompositions). Reading such state inside a composable subscribes it to changes: update the state and Compose recomposes automatically. This pair is the heart of interactive Compose.
@Composable
fun Counter() {
// remember: survive recomposition ; mutableStateOf: Compose watches it
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { // change state -> auto recompose
Text("Clicked $count times") // reading 'count' subscribes to it
}
}
// a plain 'var count = 0' would reset to 0 on every recomposition.
// remember { mutableStateOf(...) } is how you hold interactive state.The key pattern in Compose is state hoisting: move state up out of a composable and pass the value down plus an event callback up. This makes the composable stateless — it just renders what it is given and reports events — which makes it reusable, testable, and controllable by a parent (or a ViewModel). "Value down, events up" is the design principle of well-structured Compose UIs.
// STATELESS: owns no state, just renders value + reports events -> reusable
@Composable
fun CounterDisplay(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("Count: $count") }
}
// STATEFUL owner: holds the state and passes it down / handles events up
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterDisplay(count = count, onIncrement = { count++ })
}
// "value down, events up" — the stateless composable is easy to preview + test,
// and a ViewModel can own the state instead (Architecture course).remember keeps state across recompositions but not across configuration changes — rotate the device and remembered state resets, because the whole Activity is recreated. rememberSaveable persists simple state through recreation (and process death) by saving it to the same Bundle mechanism Activities use. Use it for UI state that should survive rotation, like a form field or a selected tab.
@Composable
fun NameField() {
// remember alone -> resets on rotation.
// rememberSaveable -> survives rotation + process death (saved to a Bundle).
var text by rememberSaveable { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Your name") }
)
}
// use rememberSaveable for UI state that should outlive rotation (form fields,
// selected tab). For real app data, hoist to a ViewModel (Architecture course).When a value is computed from other state, recomputing it on every recomposition can be wasteful. remember with keys caches a computation until a key changes, and derivedStateOf creates state that only updates when its computed result actually changes — useful for things like "is the button enabled?" derived from a text field. These keep recomposition efficient as UIs grow.
@Composable
fun SignUpButton(email: String) {
// recompute ONLY when 'email' changes, not every recomposition
val isValid = remember(email) { email.contains("@") && email.contains(".") }
Button(onClick = { /* submit */ }, enabled = isValid) {
Text("Sign up")
}
}
// remember(key) caches until the key changes. derivedStateOf { } is similar but
// only emits when the DERIVED result changes (great for scroll-position-driven UI).