Skip to content

Intermediate: Android basics · Navigation and app architecture · topic 28 of 50 · 35 min

ViewModel and MVVM

Explanation

MVVM splits a screen into the Model (data), the View (composables) and the ViewModel, which holds screen state and logic. A ViewModel survives configuration changes like rotation. It exposes a UiState data class through a read-only StateFlow, receives events as function calls, and runs work in viewModelScope. The UI collects the state with collectAsStateWithLifecycle().

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

PartJobIn this course
ModelThe data and the rules for getting itRepositories, Room, Retrofit (module 8)
ViewShows state and sends user actionsComposable screens
ViewModelHolds screen state, handles actions, calls the modelA ViewModel subclass with StateFlow

A ViewModel with UiState

kotlin
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!") }
        }
    }
}
One immutable UiState for the whole screen; update { it.copy(...) } changes it safely.

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

kotlin
@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) }
    }
}
viewModel() gets (or creates) the ViewModel for this screen. CartContent is stateless and easy to preview.

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.

Think of it like this: The ViewModel is like the manager of a restaurant's kitchen. Waiters (the UI) take orders to the manager (events); the manager decides what happens and updates the order board (state); waiters only read the board and serve accordingly. When the dining room is rearranged (rotation), the manager and the board stay the same.

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

Model | View | ViewModel

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.

kotlin
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) }
}
All screen logic in one testable class.

Practice (0/3 done)

Task 1 · Question

Why does data in a ViewModel survive rotation?

Task 2 · Question

Which is the correct way for the UI to change state in MVVM?

Task 3 · Practice

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

Question 1

1.What does the ViewModel expose to the UI?

Question 2

2.Which function gets a ViewModel inside a composable (without Hilt)?

Question 3

3.Why collect with collectAsStateWithLifecycle()?

Question 4

4.What is a memory leak risk in a ViewModel?

Read more (free)

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.

From Interview questions: Android and Compose

Should a repository return Compose Color or UI objects?Related

Answer: No, the data layer should not know about UI

Keep layers independent.

From App architecture: layers and repositories

Which survives screen rotation?Related

Answer: rememberSaveable

rememberSaveable stores the value in saved state.

From State and recomposition

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

  1. 2 more topics in “Navigation and app architecture” before its exam: App architecture: layers and repositories, Dependency injection with Hilt.
  2. Module 7 exam: Navigation and app architecture · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Saving data and calling APIs. 50 topics left in the course before the final exam.

← Previous: Navigation Compose