remember & mutableStateOf
In Jetpack Compose, state is the single source of truth for your UI.
Whenever the state changes, Compose automatically recomposes the parts of the UI
that depend on it. This reactive model removes the need for manual updates like
notifyDataSetChanged() in traditional Android development.
State represents data that can change over time — such as user input, animation progress, or network responses. Compose uses this state to drive the UI, ensuring it always displays the most recent data.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
Here, count is a state variable. Each time the button is clicked,
count changes — triggering recomposition, and updating the displayed value automatically.
mutableStateOf
The mutableStateOf function creates a state holder
— an observable container that notifies Compose when its value changes.
val name = mutableStateOf("Talha")
// Read value
Text(name.value)
// Update value
name.value = "Updated Name"
Compose tracks reads to name.value and will recompose any composable that
reads this value when it changes.
remember
remember ensures that state survives across recompositions.
Without it, the variable would reset to its initial value every time the UI is redrawn.
// Without remember — resets every recomposition
@Composable
fun WrongCounter() {
var count = mutableStateOf(0)
Button(onClick = { count.value++ }) {
Text("Count: ${count.value}")
}
}
Each recomposition reinitializes count, preventing persistent updates.
To fix this, use remember:
@Composable
fun CorrectCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Now the count persists and updates correctly, because Compose “remembers” the value between recompositions.
State hoisting is the practice of moving state out of a composable and passing it in as parameters. This makes components reusable and testable.
// State hoisted
@Composable
fun CounterDisplay(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterDisplay(count = count, onIncrement = { count++ })
}
Here, CounterDisplay doesn’t manage its own state —
it just displays data passed from the parent. This separation follows
unidirectional data flow (UDF).
Compose follows a one-way data flow principle:
This ensures predictable and debuggable UIs — the UI always reflects a single source of truth.
For larger apps, managing state at the composable level isn’t enough.
You can use ViewModel to hold state across configuration changes.
class CounterViewModel : ViewModel() {
var count by mutableStateOf(0)
private set
fun increment() {
count++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
Button(onClick = { viewModel.increment() }) {
Text("Count: ${viewModel.count}")
}
}
The ViewModel stores state outside the composable, ensuring
it persists during configuration changes like rotation.
remember and mutableStateOf.ViewModel and mutableStateOf.derivedStateOf to optimize recompositions.remember, causing state resets.ViewModel for screen-level state.
Now that you understand the basics of state management,
the next article will explore Side Effects & Effect Handlers —
using LaunchedEffect, rememberCoroutineScope,
and SideEffect for managing non-UI logic in a reactive way.