MVVM Core Concepts
The Problem with Tight Coupling
In a naive Android Activity, UI logic, business rules, and data fetching all live in the same class. This creates a "God Activity" that is hard to test, hard to modify, and breaks the moment requirements change. MVVM solves this by splitting responsibilities into three layers.
Model-View-ViewModel
- Model: Holds your business data and rules. It knows nothing about the UI. A
UserRepositoryreturning user profiles is part of the Model layer. - View: The Activity, Fragment, or Composable that renders UI. It observes the ViewModel and renders whatever state it exposes. The View never calls the Model directly.
- ViewModel: The bridge. It holds UI-related state, transforms Model data into something the View can render, and survives configuration changes like screen rotations.
The critical rule: data flows one direction. The View reads from the ViewModel. The ViewModel reads from the Model. The View never modifies the Model directly — it sends user intent to the ViewModel, which updates the Model, which produces new state, which the View observes.
Kotlin Code Example
// Model layer
data class User(val id: String, val name: String, val email: String)
class UserRepository(private val api: UserApi) {
suspend fun getUser(id: String): User {
return api.fetchUser(id)
}
}
// ViewModel layer
class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = repository.getUser(id)
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
sealed class UiState {
object Loading : UiState()
data class Success(val user: User) : UiState()
data class Error(val message: String) : UiState()
}
The Activity only observes uiState and renders the appropriate UI. It has zero knowledge of where the user data comes from.
Unidirectional Data Flow in Practice
Why One Direction?
Bidirectional data flow creates debugging nightmares. When the View writes to the ViewModel and the ViewModel writes to the View, tracking a bug means following state mutations across multiple classes. Unidirectional flow means state changes happen in one place — the ViewModel — and the View simply observes.
The Data Flow Cycle
- User taps a button → View calls a ViewModel method
- ViewModel processes intent → Calls repository or business logic
- Model produces new data → ViewModel wraps it in a state class
- ViewModel emits new state → View observes and re-renders
Common State Patterns
// Using sealed class for all possible UI states
sealed interface HomeUiState {
data object Loading : HomeUiState
data class Loaded(val items: List<Item>) : HomeUiState
data class Error(val reason: String) : HomeUiState
}
// Single source of truth for screen state
data class ProfileUiState(
val userName: String = "",
val isLoading: Boolean = false,
val avatarUrl: String? = null,
val isEditing: Boolean = false
)
// ViewModel updates the entire state object
class ProfileViewModel : ViewModel() {
private val _state = MutableStateFlow(ProfileUiState())
val state: StateFlow<ProfileUiState> = _state.asStateFlow()
fun onNameChanged(newName: String) {
_state.update { it.copy(userName = newName) }
}
fun onSaveClicked() {
_state.update { it.copy(isLoading = true) }
viewModelScope.launch {
// save logic...
_state.update { it.copy(isLoading = false) }
}
}
}
LiveData vs StateFlow
Both work for MVVM. StateFlow is preferred in new projects because it integrates with Kotlin coroutines and Compose natively. LiveData works well with XML views and lifecycle awareness is built in.
// StateFlow (modern approach)
val state: StateFlow<UiState> = _state.asStateFlow()
// LiveData (XML view approach)
val state: LiveData<UiState> = _state.asLiveData()
MVVM Pitfalls and Testability
Common MVVM Mistakes
- Fat ViewModel: Putting business logic, navigation, and data transformation all in the ViewModel. Business logic belongs in Use Cases or the Model layer.
- View directly mutating ViewModel state: The View should call a method; the ViewModel decides how state changes.
- Ignoring state: Exposing multiple separate LiveData streams instead of a single state object leads to inconsistent UI states.
- Holding View references: A ViewModel must never hold a reference to an Activity, Fragment, or Context. It survives configuration changes — if it held a View reference, it would leak the old Activity.
Testing MVVM
MVVM makes testing straightforward because the ViewModel has no Android framework dependencies.
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private lateinit var viewModel: UserViewModel
@Before
fun setup() {
viewModel = UserViewModel(repository)
}
@Test
fun `loadUser success updates state`() = runTest {
val user = User("1", "Ada", "ada@test.com")
coEvery { repository.getUser("1") } returns user
viewModel.loadUser("1")
val state = viewModel.uiState.value
assert(state is UiState.Success)
assertEquals(user, (state as UiState.Success).user)
}
@Test
fun `loadUser failure updates error state`() = runTest {
coEvery { repository.getUser("1") } throws IOException("Network error")
viewModel.loadUser("1")
val state = viewModel.uiState.value
assert(state is UiState.Error)
}
}
No Robolectric, no instrumentation tests, no mocked Android framework. Pure JVM unit tests.
Quiz
1. In MVVM, where should business logic reside?
2. Why must a ViewModel never hold a reference to an Activity?
3. What is the correct data flow direction in MVVM?
4. Which is preferred for new Android projects using MVVM with Compose?
Flashcards
Question
What are the three layers of MVVM?
Click to reveal answer
Answer
Model (business data and rules), View (UI rendering), ViewModel (presentation logic bridging Model and View).
Question
Why is unidirectional data flow important in MVVM?
Click to reveal answer
Answer
It makes state changes predictable and debuggable. All mutations happen in one place (the ViewModel), so you only need to trace one path when a bug occurs.
Question
Why can't a ViewModel hold a Context or View reference?
Click to reveal answer
Answer
ViewModels survive configuration changes. Holding a View reference would prevent the old Activity from being garbage collected, causing a memory leak.
Question
What is the advantage of using a single sealed state class over multiple LiveData fields?
Click to reveal answer
Answer
A single state class guarantees consistency — all UI properties are updated atomically. Multiple separate streams can lead to contradictory intermediate states.
Revision Notes
Key Takeaways
- 1. MVVM separates UI, presentation logic, and data into distinct layers
- 2. Unidirectional data flow makes state changes predictable and debuggable
- 3. ViewModels survive configuration changes but must never hold View references
- 4. A single sealed state class per screen prevents inconsistent UI states
Interview Tips
- • Explain MVVM by contrasting it with MVC — where the View directly modifies the Model
- • Be ready to draw the unidirectional data flow diagram on a whiteboard
- • Discuss why you'd choose StateFlow over LiveData for new projects
- • Know the difference between ViewModel scope and CoroutineScope
Cheat Sheet
MVVM Cheat Sheet
Layers:
- View (Activity/Fragment/Composable): Renders UI, observes ViewModel
- ViewModel: Holds UI state, survives config changes, calls Model
- Model (Repository/UseCase): Business logic and data
Data Flow: User intent → ViewModel method → Model call → new state → View renders
Key Rules:
- ViewModel never holds View/Context references
- View never calls Model directly
- One ViewModel per screen (or sub-screen)
- Single state object per screen
State Options:
MutableStateFlow+collectAsState(Compose)MutableLiveData+observe(XML views)
Testing: ViewModels are pure Kotlin classes — test with plain JVM unit tests, no Android framework needed.