Jetpack Compose introduces a very different mental model compared to XML-based Android UI. One line that often confuses beginners (and even experienced Android devs at first) is:
onValueChange = { value = it }Especially when value is defined like this:
var value by remember { mutableStateOf(0) }At first glance, this line looks almost too simple — and that’s exactly why it’s confusing.
Let’s break down what’s really happening, why it’s written this way, and how it fits into Compose’s state-driven architecture.
The Big Picture: Compose Is State-Driven
Before diving into syntax, it’s important to understand how Compose thinks.
In classic Android:
- You updated UI elements directly
- UI held its own state
- You manually synced UI ↔ data
In Jetpack Compose:
- State owns the UI
- UI is a function of state
- When state changes → UI recomposes automatically
This single line:
onValueChange = { value = it }is the bridge between user interaction and state updates.
What remember { mutableStateOf(...) } Really Does
Consider this state declaration:
var value by remember { mutableStateOf(0) }This does three important things:
1. mutableStateOf
Creates an observable state holder.
Compose watches this value and tracks where it’s used.
2. remember
Ensures the state survives re-composition.
Without remember, the value would reset every time Compose redraws the UI.
3. by keyword
This is Kotlin property delegation. It allows you to write:
value = 5instead of:
value.value = 5So value behaves like a normal variable, but Compose is quietly observing it.
What onValueChange Is (Conceptually)
Most interactive Compose components (such as TextField, Slider, Checkbox) follow the same pattern:
Component(
value = currentState,
onValueChange = { /* update state */ }
)This is intentional and consistent.
onValueChange is:
- A callback function
- Triggered every time the user interacts
- Passed the new value as a parameter
Compose itself does not store the value internally.
You are responsible for updating the state.
Breaking Down { value = it }
Let’s rewrite the lambda in a more explicit way:
onValueChange = { newValue ->
value = newValue
}Now it’s clearer.
it(ornewValue) is the latest value from the UIvalue = itupdates your state- Updating state triggers recomposition
This is not assigning a random variable — it’s updating the single source of truth.
How the Data Flow Actually Works
Here’s the real flow behind the scenes:
- User interacts with the UI (types text, drags slider, etc.)
- Compose calls
onValueChange(newValue) - You update state (
value = newValue) - Compose detects the state change
- Any composables reading
valuerecompose automatically
This is called unidirectional data flow, and it’s a core Compose principle.
State → UI<br>UI interaction → Callback → State update → RecompositionSimple Example with TextField
@Composable
fun NameInput() {
var name by remember { mutableStateOf("") }
TextField(
value = name,
onValueChange = { name = it },
label = { Text("Enter your name") }
)
}Here,
namecontrols what the TextField displays- Typing triggers
onValueChange - The new text is assigned to
name - The TextField redraws with updated text
If you remove onValueChange, the field becomes read-only.
Why Compose Doesn’t Update the Value Automatically
This design is intentional.
Compose avoids hidden internal state because:
- It prevents bugs
- It makes UI predictable
- It improves testability
- It aligns with modern architecture (MVI, Redux-style patterns)
You always know where your data lives.
Common Beginner Mistakes
Forgetting to update state
onValueChange = { }Result: UI never changes.
Not using remember
var value by mutableStateOf(0)Result: Value resets on every recomposition.
Expecting Compose to “save” the value
Compose renders, it doesn’t store business state. That’s your job (or ViewModel’s).
Why This Pattern Is So Powerful
Once you understand this line, you understand 50% of Compose.
It enables:
- Clean separation of UI and state
- Easy state hoisting
- Predictable recomposition
- Seamless ViewModel integration
Example with state hoisting:
@Composable
fun Counter(value: Int, onValueChange: (Int) -> Unit) {
Slider(
value = value.toFloat(),
onValueChange = { onValueChange(it.toInt()) }
)
}Now the parent owns the state — not the UI.
Final Mental Model (Remember This)
Compose does not mutate UI.
Compose reacts to state changes.
And this line:
onValueChange = { value = it }is simply saying:
“When the user changes something, update my state — and let Compose handle the rest.”
Once this clicks, Jetpack Compose stops feeling confusing and starts feeling refreshingly simple..!
