Intermediate: Android basics · Navigation and app architecture · topic 28 of 50 · 35 min
ViewModel and MVVM
Explanation
Why a ViewModel?
If a screen's data and logic live in composables or the activity, two problems appear. First, rotation recreates the activity and the data is lost or reloaded. Second, logic mixed with UI code is hard to test. A ViewModel is a class that holds the state and logic for a screen and survives configuration changes. It is cleared only when the screen is gone for good (for example, the user navigates away and it is popped from the back stack).
MVVM in plain words
| Part | Job | In this course |
|---|---|---|
| Model | The data and the rules for getting it | Repositories, Room, Retrofit (module 8) |
| View | Shows state and sends user actions | Composable screens |
| ViewModel | Holds screen state, handles actions, calls the model | A ViewModel subclass with StateFlow |
A ViewModel with UiState
data class CartUiState(
val items: List<String> = emptyList(),
val isCheckingOut: Boolean = false,
val message: String? = null,
) {
val count: Int get() = items.size
}
class CartViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CartUiState())
val uiState: StateFlow<CartUiState> = _uiState.asStateFlow()
fun onAdd(item: String) {
_uiState.update { it.copy(items = it.items + item) }
}
fun onCheckout() {
viewModelScope.launch {
_uiState.update { it.copy(isCheckingOut = true) }
delay(1000) // pretend payment call
_uiState.update { CartUiState(message = "Order placed!") }
}
}
}A UiState data class collects everything the screen needs to draw. Because it is immutable, every change creates a new object with copy, and StateFlow tells the UI. update { } is a thread-safe way to change a MutableStateFlow's value based on the old one.
Using it from Compose
@Composable
fun CartScreen(viewModel: CartViewModel = viewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
CartContent(state = state, onAdd = viewModel::onAdd, onCheckout = viewModel::onCheckout)
}
@Composable
fun CartContent(state: CartUiState, onAdd: (String) -> Unit, onCheckout: () -> Unit) {
Column(Modifier.padding(16.dp)) {
Text("${state.count} items")
Button(onClick = { onAdd("Mango pickle") }) { Text("Add pickle") }
Button(onClick = onCheckout, enabled = state.count > 0 && !state.isCheckingOut) { Text("Checkout") }
state.message?.let { Text(it) }
}
}collectAsStateWithLifecycle() (from the lifecycle-runtime-compose library) collects the flow only while the screen is at least STARTED, so nothing runs in the background when the app is hidden.
Splitting CartScreen (which talks to the ViewModel) from CartContent (which only takes state and lambdas) is a common pattern. Previews and UI tests use CartContent with sample state, with no ViewModel needed.
Events in, state out
The UI never changes the state directly. It calls functions on the ViewModel (events such as onAdd, onCheckout), and the ViewModel updates the state. The UI just draws whatever state it receives. This one-way loop makes bugs easy to trace: every change happens in one place.
Common mistakes
- Holding a reference to an Activity, Context or composable inside the ViewModel: memory leaks. A ViewModel outlives the activity.
- Exposing MutableStateFlow publicly: any screen can change state anywhere.
- Several separate flows for one screen that can disagree. Prefer one UiState.
- Doing network calls in composables instead of the ViewModel.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
ViewModel and MVVM · 1/7
MVVM splits a screen into Model, View and ViewModel.
Prefer a video by a teacher? Find free videos on “android viewmodel stateflow mvvm jetpack compose uistate” on YouTube ↗
In real life: A recharge screen
A mobile recharge app's screen has a number field, a list of plans and a Pay button. The RechargeViewModel holds RechargeUiState(number, plans, selectedPlan, isPaying, error). Typing calls onNumberChange, which validates and loads plans for the operator. When the user rotates the phone mid-payment, the ViewModel and its coroutine continue, so the payment is not started twice.
data class RechargeUiState(val number: String = "", val selectedPlan: Int? = null, val isPaying: Boolean = false)
class RechargeViewModel : ViewModel() {
private val _state = MutableStateFlow(RechargeUiState())
val state: StateFlow<RechargeUiState> = _state.asStateFlow()
fun onNumberChange(n: String) = _state.update { it.copy(number = n.filter(Char::isDigit).take(10)) }
fun onPlanSelected(price: Int) = _state.update { it.copy(selectedPlan = price) }
}Practice (0/3 done)
Why does data in a ViewModel survive rotation?
Which is the correct way for the UI to change state in MVVM?
Build a QuizViewModel with QuizUiState(questionIndex, score, finished). Functions: onAnswer(correct: Boolean) moves to the next question and adds to the score; onRestart resets. Build a QuizScreen that collects the state with collectAsStateWithLifecycle and a stateless QuizContent. Rotate mid-quiz to check the score is kept.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- App architecture: layers and repositoriesNavigation and app architecture · same module
- Dependency injection with HiltNavigation and app architecture · same module
- Interview questions: Android and ComposeAndroid career path
- Activities and the lifecycleAndroid Studio and your first app
- Hello Compose: your first screenAndroid Studio and your first app
- Unit tests with JUnit and coroutinesTesting Android apps
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does the ViewModel expose to the UI?
Answer: A read-only StateFlow of UI state
Expose read-only state.
Which function gets a ViewModel inside a composable (without Hilt)?
Answer: viewModel()
viewModel() returns the instance scoped to the screen.
Why collect with collectAsStateWithLifecycle()?
Answer: It stops collecting when the screen is not visible
Lifecycle-aware collection saves resources.
What is a memory leak risk in a ViewModel?
Answer: Holding a reference to an Activity or Context
The ViewModel outlives the activity.
Which is best for a one-off event like "navigate to the payment screen"?Related
Answer: SharedFlow (or a state that is cleared after handling)
Events should not be replayed like state.
From Flow and StateFlow
Why must a ViewModel not hold an Activity reference?Related
Answer: The ViewModel outlives the activity, causing a memory leak
The activity is recreated on rotation.
Should a repository return Compose Color or UI objects?Related
Answer: No, the data layer should not know about UI
Keep layers independent.
Which survives screen rotation?Related
Answer: rememberSaveable
rememberSaveable stores the value in saved state.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Navigation and app architecture” before its exam: App architecture: layers and repositories, Dependency injection with Hilt.
- Module 7 exam: Navigation and app architecture · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Saving data and calling APIs. 50 topics left in the course before the final exam.