Compose Fundamentals Part 3: State and Recomposition
The Heart of Compose: State
So far, our Composables have been static — they display data but cannot change. Real apps need interactive UIs: buttons that respond, counters that increment, forms that update. This requires state.
State is any value that changes over time. In Compose, when state changes, the framework automatically updates the UI. This is called recomposition.
Your First Stateful Component
Let us build a simple counter. We need two things: a variable to hold the count, and a way to tell Compose when it changes:
@Composable
fun Counter() {
// remember: Keep this value across recompositions
// mutableStateOf: When this changes, trigger recomposition
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Button(onClick = { count-- }) {
Text("-")
}
Spacer(modifier = Modifier.width(16.dp))
Button(onClick = { count++ }) {
Text("+")
}
}
}
}Breaking Down remember and mutableStateOf
These two functions are the foundation of Compose state:
- mutableStateOf(value): Creates an observable state holder. When you change it, Compose knows to update the UI.
- remember { }: Saves a value across recompositions. Without it, your state would reset every time the function runs.
- Together, they create persistent, observable state that survives UI updates.
What Is Recomposition?
Recomposition is Compose re-running your Composable functions when state changes. But here is the genius: Compose is smart about it.
@Composable
fun ParentScreen() {
var name by remember { mutableStateOf("") }
Column {
// This recomposes when name changes
NameInput(
name = name,
onNameChange = { name = it }
)
// This does NOT recompose - it doesn't read "name"
StaticHeader()
// This recomposes when name changes
Greeting(name = name)
}
}Common State Patterns
Here are patterns you will use frequently:
// Text input
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it }
)
// Toggle/checkbox
var isChecked by remember { mutableStateOf(false) }
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it }
)
// List of items
var items by remember { mutableStateOf(listOf("A", "B", "C")) }
items = items + "D" // Add item
items = items.filter { it != "B" } // Remove itemState vs Stateless Composables
As you gain experience, you will learn to separate stateful and stateless Composables. Stateless Composables are easier to test, preview, and reuse:
// Stateless: receives state as parameter
@Composable
fun CounterDisplay(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
// Just displays, doesn't own state
}
// Stateful: manages its own state
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterDisplay(
count = count,
onIncrement = { count++ },
onDecrement = { count-- }
)
}