ViewModel & State Management

Android Development โ€” Architecture & Data
Course 2 ยท Chapter 2 ยท ViewModel & State Management

๐Ÿ›๏ธ ViewModel & State Management

Last chapter's Counter kept its count inside remember { mutableStateOf(0) } โ€” fine for a toy example, but that state resets on rotation (Course 1, Chapter 2), and it can't be shared between composables that aren't nested together. ViewModel solves both problems, and StateFlow (already covered fully in Kotlin Intermediate Chapter 2) is how it exposes state to the UI. This chapter is largely about combining tools already learned, not new syntax.

๐Ÿค” 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

class CounterViewModel : ViewModel() { private val _count = MutableStateFlow(0) val count: StateFlow<Int> = _count.asStateFlow() fun increment() { _count.value++ } }

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

@Composable fun CounterScreen(viewModel: CounterViewModel = viewModel()) { val count by viewModel.count.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { Text(text = "Count: $count") Button(onClick = { viewModel.increment() }) { Text("Increment") } } }

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.

โš  Rotate the Emulator to See the Difference

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:

// The LiveData equivalent of this chapter's StateFlow ViewModel class CounterViewModel : ViewModel() { private val _count = MutableLiveData(0) val count: LiveData<Int> = _count fun increment() { _count.value = (_count.value ?: 0) + 1 } }

LiveData vs StateFlow

LiveDataStateFlow
Lifecycle-aware?Yes, built inNo โ€” needs collectAsStateWithLifecycle() in Compose
Kotlin coroutines integrationBolted on (asFlow())Native โ€” it IS a coroutines Flow type
Operators (map, combine, ...)LimitedFull Flow operator set (Kotlin Intermediate Ch2)
Current statusLegacy, still widely usedModern 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:

data class ProfileUiState( val isLoading: Boolean = false, val userName: String? = null, val errorMessage: String? = null ) class ProfileViewModel : ViewModel() { private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true)) val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow() fun onLoadSuccess(name: String) { // copy() from Kotlin Fundamentals Chapter 4 โ€” update just what changed _uiState.value = _uiState.value.copy(isLoading = false, userName = name) } }

_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 treeA storeA ViewModel
Single state object per screenA reducer's state shapeA UiState data class
Survives re-render / rotationStore persists across re-renders inherentlyViewModel persists across configuration changes
Subscribing from UIuseSelector() / a store hookcollectAsStateWithLifecycle()

๐Ÿ’ป 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.

โ†’ Solution

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.

โ†’ Solution

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.

โ†’ Solution

๐Ÿ’ก Nothing Here Was Genuinely New Syntax

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.