MVC vs MVP vs MVVM vs MVI on Android — which and why
Every architecture pattern promises testable, decoupled UI code — interviewers use this question to check whether you understand the tradeoffs behind the acronyms, not just the acronyms themselves.
LEARN BY DOINGPut your intuition to work3 steps
MVC vs MVP vs MVVM vs MVI on Android — which and why
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Why this question keeps coming up
Every Android interview beyond the junior level eventually asks some version of "compare MVC, MVP, MVVM, and MVI." It looks like a vocabulary check, but it isn't — it's a proxy for whether you understand separation of concerns, whether you can explain testability without waving at "unit tests" in the abstract, and whether you know how each pattern actually interacts with the Activity/Fragment lifecycle, which is where these patterns tend to break in real apps. A junior answer lists four acronyms. A senior answer explains what problem each pattern solved relative to the one before it, and can say — concretely, for a specific screen — which one they would reach for and why.
#MVC: the pattern Android borrowed the name from, not the shape
Android is often called "MVC by default," but it never had a clean MVC in the classic desktop sense. The Activity or Fragment ends up playing both View and Controller — it inflates the layout, holds references to widgets, and also contains the click handlers and business logic. The Model is whatever plain data classes or repositories you define yourself. There is no framework-enforced boundary, which is exactly the problem: the Activity becomes a "God object" that is difficult to unit test, because touching any of its logic means touching Android framework classes that only run correctly on a device or in Robolectric.
#MVP: pull the logic out, keep the View dumb
Model-View-Presenter fixes the testability problem by introducing a Presenter that holds all the logic and talks to the View only through a narrow interface (interface UserView { fun showUser(user: User); fun showError(message: String) }). The Activity implements that interface and does nothing but render — no decisions, no logic. Because the Presenter has no Android dependencies, you can unit test it with plain JUnit and a mock View. The tradeoff is boilerplate: every screen needs a View interface, a Presenter, and careful lifecycle management, because the Presenter holds a reference to the View and must null it out in onDestroy or you leak the Activity.
#MVVM: no reference back to the View at all
Model-View-ViewModel removes even that reference. The ViewModel exposes observable state — historically LiveData, now more often StateFlow — and the View (Activity/Fragment/Composable) subscribes to it. The ViewModel never holds a pointer to the View, so there is nothing to leak and nothing to null out. Jetpack's ViewModel class adds a second, very practical benefit on top of the pattern: it survives configuration changes automatically, because the framework retains the ViewModelStore across Activity recreation. This is why Google recommends MVVM as the default for most screens — you get the architecture pattern and a lifecycle-aware component in one class. The classic MVVM pitfall is one-off events (navigation, snackbars, toasts): naively re-emitting the last value on every new subscriber re-fires the event on rotation, which is why teams reach for SharedFlow with no replay, Channel, or a wrapped "consumed once" event type.
#MVI: one state, one direction, no exceptions
Model-View-Intent takes the idea of "the View doesn't hold its own state" to its logical conclusion: the entire screen has exactly one immutable State object, and the only way to change it is by dispatching an Intent (a user action, not to be confused with the Android Intent class) that a reducer turns into a new State. Nothing mutates in place; every update replaces the whole State. This makes the system trivial to reason about and to test — given a State and an Intent, the next State is a pure function — and it maps very naturally onto Compose, which is itself a unidirectional, state-driven rendering model. The cost is ceremony: even a screen with one boolean toggle needs a State data class, a sealed Intent hierarchy, and a reducer, which can feel like overkill on a simple form.
#Side by side
| Pattern | View holds | Testable without Android | Handles config change | Typical boilerplate |
|---|---|---|---|---|
| MVC | logic + refs | Poor | No | Low |
| MVP | interface only | Good | No (needs care) | Medium-high |
| MVVM | subscription to state | Good | Yes (ViewModel) | Medium |
| MVI | subscription to one State | Very good | Yes (ViewModel) | High |
#A minimal MVVM example
data class ProfileUiState(val isLoading: Boolean = true,val userName: String? = null,val errorMessage: String? = null)class ProfileViewModel(private val repository: UserRepository) : ViewModel() {private val _uiState = MutableStateFlow(ProfileUiState())val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()fun loadProfile(userId: String) {viewModelScope.launch {_uiState.update { it.copy(isLoading = true, errorMessage = null) }runCatching { repository.getUser(userId) }.onSuccess { user ->_uiState.update { it.copy(isLoading = false, userName = user.name) }}.onFailure { error ->_uiState.update { it.copy(isLoading = false, errorMessage = error.message) }}}}}@Composablefun ProfileScreen(viewModel: ProfileViewModel) {val state by viewModel.uiState.collectAsStateWithLifecycle()when {state.isLoading -> LoadingSpinner()state.errorMessage != null -> ErrorMessage(state.errorMessage)else -> Text(text = "Hello, ${state.userName}")}}
Notice the ViewModel never imports anything from android.view or holds an Activity/Fragment reference — it only knows about UserRepository and exposes a StateFlow. That's what makes it trivially testable: swap in a fake repository, call loadProfile, and assert on uiState.value.
🧠 Check your understanding
0/3 · 0/3 answered1. What is the main testability advantage MVP and MVVM share over plain MVC on Android?
2. Why does MVP require careful handling in onDestroy that MVVM does not?
3. What distinguishes MVI from MVVM most sharply?