- Explain the difference between
remember,rememberSaveableandmutableStateOf - Hoist state and write a stateless composable
- Keep screen state in a ViewModel with
StateFlowand read it in Compose - Describe unidirectional data flow and the MVVM layers
Leyla has reached the third question in a quiz app and turns her phone sideways — the score resets and she has to start again. All bugs like this have the same root: the state was not kept in the right place. State is any value that can change over time and affects the screen: typed text, a switch position, a loaded list, a score. In this lesson you will learn where and how to keep it.
remember and mutableStateOf
Compose only notices changes to observable state. mutableStateOf(...) creates such a value: when you change it, Compose calls the functions that read it again (recomposition). remember { } keeps the value between recompositions; without it the function would start from scratch on every call. The by keyword lets you work with it without writing .value.
@Composable
fun Counter() {
var count = 0
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}@Composable
fun Counter() {
var count by rememberSaveable { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("Clicked $count times")
}
}count grows, but Compose never learns about it and the button always shows 0. On the right every tap updates the screen, and the value even survives a screen rotation.State hoisting
Moving state out of a composable into the function that calls it. The composable receives the value as a parameter and reports change requests upward through a lambda. Such a function becomes stateless: it is easy to test, preview and reuse.
@Composable
fun CounterScreen() {
var count by rememberSaveable { mutableIntStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
@Composable
fun Counter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Button(onClick = onIncrement, modifier = modifier) {
Text("Clicked $count times")
}
}CounterScreen owns the state, while Counter only shows it and reports taps.ViewModel and StateFlow
A ViewModel is a class that holds a screen's data and logic. It is not recreated when the screen rotates, so the score is not lost. We expose the state through a StateFlow — a flow that always has a current value and broadcasts changes. Inside, a changeable MutableStateFlow is kept private, and only a read-only StateFlow is exposed.
data class QuizUiState(
val question: Int = 1,
val score: Int = 0,
val finished: Boolean = false
)
class QuizViewModel : ViewModel() {
private val _uiState = MutableStateFlow(QuizUiState())
val uiState: StateFlow<QuizUiState> = _uiState.asStateFlow()
fun answer(correct: Boolean) {
_uiState.update { state ->
val next = state.question + 1
state.copy(
question = next,
score = if (correct) state.score + 1 else state.score,
finished = next > 5
)
}
}
}data class; update atomically replaces it with a new copy.@Composable
fun QuizScreen(viewModel: QuizViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
Column(modifier = Modifier.padding(16.dp)) {
if (state.finished) {
Text("Your score: ${state.score} / 5")
} else {
Text("Question ${state.question} of 5")
Button(onClick = { viewModel.answer(correct = true) }) { Text("Correct") }
Button(onClick = { viewModel.answer(correct = false) }) { Text("Wrong") }
}
}
}The viewModel() function creates the ViewModel for the screen or returns the existing one. collectAsStateWithLifecycle() turns the flow into Compose state and stops collecting while the app is in the background. These functions come from the lifecycle-viewmodel-compose and lifecycle-runtime-compose libraries.
- 1Add them to the catalog
In
gradle/libs.versions.toml, add two lines to the[libraries]section:androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } - 2Add them to the module
In
app/build.gradle.kts, addimplementation(libs.androidx.lifecycle.viewmodel.compose)andimplementation(libs.androidx.lifecycle.runtime.compose)to thedependenciesblock. - 3Sync and import
Press
Sync Now, then put the cursor on the redviewModelandcollectAsStateWithLifecycle, pressAlt+Enterand chooseimport.
Unidirectional data flow and MVVM
In the code above, data moves in a circle: the screen shows the state → the user taps a button → the screen sends an event to the ViewModel (answer) → the ViewModel creates a new state → the screen shows it. This is called unidirectional data flow (UDF). Because only one place changes the state, bugs become easier to find. You can check this logic with plain Kotlin too:
data class CounterState(val count: Int = 0, val message: String = "")
sealed interface CounterEvent {
data object Increment : CounterEvent
data object Reset : CounterEvent
}
fun reduce(state: CounterState, event: CounterEvent): CounterState = when (event) {
CounterEvent.Increment -> state.copy(
count = state.count + 1,
message = if (state.count + 1 >= 3) "Great streak!" else ""
)
CounterEvent.Reset -> CounterState()
}
fun main() {
var state = CounterState()
val events = listOf(CounterEvent.Increment, CounterEvent.Increment, CounterEvent.Increment, CounterEvent.Reset)
for (event in events) {
state = reduce(state, event)
println("$event -> $state")
}
}Increment -> CounterState(count=1, message=) Increment -> CounterState(count=2, message=) Increment -> CounterState(count=3, message=Great streak!) Reset -> CounterState(count=0, message=)
| MVVM layer | Job | Example |
|---|---|---|
| Model (data) | gets data from the internet or a database and stores it | LessonRepository |
| ViewModel | holds the screen state and handles events | QuizViewModel |
| View | shows the state and passes events to the ViewModel | QuizScreen |
Context directly.Cmd+Option+M)Ctrl+Alt+MOption+F7)Alt+F7Cmd+F12)Ctrl+F12Key points
- Compose only sees changes to observable state such as
mutableStateOf;rememberkeeps it between recompositions. rememberSaveablekeeps simple values even through a rotation; the screen's main data lives in a ViewModel.- Hoist state: the
valuegoes down, events such asonValueChangego up. - A ViewModel keeps state in a private
MutableStateFlowand exposes a publicStateFlow; the screen reads it withcollectAsStateWithLifecycle(). - MVVM: the Model provides data, the ViewModel manages state, the View shows it — data moves in one direction.
Check yourself
10 questions. Every correct answer earns XP.
remember { mutableStateOf(0) } when the screen rotates?