ViewModel & State Management
๐๏ธ ViewModel & State Management
๐ค Why remember Isn't Always Enough
remember ties state to a composable's position in the UI tree โ it survives recomposition, but not a configuration change like rotation, which (per Course 1, Chapter 2) can destroy and recreate the entire Activity. A ViewModel is deliberately scoped differently: it survives configuration changes automatically, and is destroyed only when its owning screen is genuinely, permanently gone (not just rotated).
๐๏ธ Defining a ViewModel
The private var _count / public val count naming pattern is a deliberate encapsulation convention: the ViewModel exposes a read-only StateFlow externally, while keeping the mutable version private to itself โ nothing outside the ViewModel can directly set count, only call increment(). This is the same underscore-prefix convention commonly used for backing properties, applied specifically to the StateFlow pattern from Kotlin Intermediate Chapter 2.
๐ Using a ViewModel from Compose
viewModel = viewModel() is a default parameter that fetches (or creates, on first call) the correctly-scoped CounterViewModel instance automatically โ this is the mechanism that makes the ViewModel survive rotation, since the same instance is returned again after recreation rather than a fresh one being constructed. collectAsStateWithLifecycle() bridges a StateFlow into Compose's own state system, using by delegation exactly like remember { mutableStateOf(...) } did โ the composable recomposes automatically whenever the ViewModel's count emits a new value.
Swap Chapter 1's Counter() (using plain remember) for this chapter's CounterScreen() (using a ViewModel) and rotate the emulator after a few clicks โ Chapter 1's count resets to 0; this chapter's survives. That side-by-side comparison, run for real, makes the entire reason ViewModel exists concrete rather than abstract.
๐ LiveData โ Where You'll Still See It
Before StateFlow became idiomatic, LiveData was Android's standard observable state holder โ it still appears constantly in existing codebases and some libraries, so it's worth recognizing even though new code generally prefers StateFlow:
LiveData vs StateFlow
| LiveData | StateFlow | |
|---|---|---|
| Lifecycle-aware? | Yes, built in | No โ needs collectAsStateWithLifecycle() in Compose |
| Kotlin coroutines integration | Bolted on (asFlow()) | Native โ it IS a coroutines Flow type |
| Operators (map, combine, ...) | Limited | Full Flow operator set (Kotlin Intermediate Ch2) |
| Current status | Legacy, still widely used | Modern default for new code |
๐ฆ A UI State Pattern: One Data Class Per Screen
Rather than several separate loose StateFlows (one for loading, one for data, one for an error message โ easy to get into an inconsistent combination), a common pattern bundles a screen's entire state into one data class:
_uiState.value.copy(...) is Kotlin Fundamentals Chapter 4's data class copy(), doing real work here โ every state update produces a new, complete, internally-consistent ProfileUiState instance rather than mutating individual fields piecemeal, which is exactly what makes it impossible to accidentally end up with isLoading = true and a populated userName at the same time.
ViewModel + StateFlow vs Frontend State Management
| React (Redux/Zustand-style) | Android ViewModel | |
|---|---|---|
| Holds state outside the UI tree | A store | A ViewModel |
| Single state object per screen | A reducer's state shape | A UiState data class |
| Survives re-render / rotation | Store persists across re-renders inherently | ViewModel persists across configuration changes |
| Subscribing from UI | useSelector() / a store hook | collectAsStateWithLifecycle() |
๐ป Coding Challenges
Challenge 1: A ViewModel-Backed Counter
Rewrite Chapter 1's Counter composable to use a CounterViewModel (as shown in this chapter) instead of remember. Run it, increment several times, rotate the emulator, and confirm the count survives โ unlike Chapter 1's version.
Goal: Directly experience the rotation-survival difference ViewModel provides.
Challenge 2: A UiState Data Class
Write a data class TodoUiState(val items: List<String> = emptyList(), val isLoading: Boolean = true) and a TodoViewModel exposing it as a StateFlow. Add a function addItem(text: String) that updates the state via copy(), appending the new item and setting isLoading to false.
Goal: Practice the single-data-class UI state pattern with copy()-based updates.
Challenge 3: Collecting UiState in a Composable
Write a TodoScreen composable that collects TodoUiState from Challenge 2's TodoViewModel using collectAsStateWithLifecycle(), shows a loading Text while isLoading is true, and otherwise shows each item using a Text per entry inside a Column, plus a Button that calls addItem() with sample text.
Goal: Practice the full ViewModel-to-Compose data flow, conditionally rendering based on state.
StateFlow, MutableStateFlow, and data class copy() were all fully covered in the Kotlin courses โ this chapter's real content is the architectural pattern (ViewModel as the state owner, a single UiState per screen, Compose as a pure function of that state), not new language features. That's a deliberate sign the earlier courses did their job: Android-specific chapters increasingly become "apply what you already know" rather than "learn something brand new."
๐ฏ What's Next
Next chapter: Room Database โ entities, DAOs, the database class, and coroutines with Room.