When We Talk About MVP, MVVM, and MVI — What Are We Really Talking About?
💡 Introduction
When we talk about MVP, MVVM, or MVI,
What we are really discussing is:
- A UI layer architecture pattern (Presentation Layer Architecture Pattern)
- Specifically, how responsibilities are divided and how data flows within the presentation layer.
These are forms of decoupled architectural patterns for managing UI data flow.
📦 Background: Why Do We Need These Patterns?
In early UI development (whether Android, iOS, or Web), a common issue was:
- All logic lived in one massive controller,
Activity
, orViewController
.
- All logic lived in one massive controller,
This led to:
- Heavy coupling between UI and business logic;
- Poor testability;
- Low reusability;
- Difficult state management.
Hence, the goal of architectural patterns is to:
- Structure the UI layer more clearly,
- Clarify data flow, and
- Make state management predictable and controllable.
MVC
The most traditional approach is MVC.
- In Android’s view system, layout files are defined in XML, while
Activity
orFragment
act as the Controller, responsible for refreshing UI data. - However, due to the limited capability of XML layouts, many UI behaviors have to be implemented directly inside
Activity
/Fragment
, making the Controller also responsible for part of the View logic. - If data fetching also stays inside
Activity
/Fragment
, the code becomes bloated and difficult to maintain.
- In Android’s view system, layout files are defined in XML, while
Therefore, Google officially recommended using MVP as a replacement for traditional MVC.
MVP
- MVP uses contracts and interfaces to implement data callbacks between layers.
- In MVP, the
Presenter
takes over data generation from theView
, requests data through the service layer, and returns the result back to theView
for rendering. - However, this pattern has its own drawbacks.
While many argue that the Presenter becomes too heavy, this is not an inherent problem of MVP itself. It’s actually because developers didn’t properly separate the data control layer into smaller components such as Repository, UseCase, etc. This fine-grained separation is necessary even in MVVM.
In my opinion, the main reason MVP gradually faded out is:
Presenter–View dependency injection and lifecycle management had to be done manually.
This made MVP fragile, depending heavily on how each team understood and implemented it.
Common issues:
Some developers implemented MVP too simplistically:
- They only set up the data callback (Presenter → View) without properly binding the lifecycle.
- As a result, when a View is destroyed while a network request is still ongoing, the Presenter might try to update a destroyed View, causing NullPointerExceptions or memory leaks.
In Kotlin, null-safety reduces crashes, but the leaks and potential ANRs still exist.
Another pain point: MVP relies on a large number of interfaces for callbacks. While this is idiomatic Java, it makes tracing business logic cumbersome and verbose.
To solve these problems, Google introduced ViewModel in Jetpack — a lifecycle-aware component with a much simpler usage pattern.
For a detailed implementation of MVP, see here.
MVVM
- MVVM is a unidirectional data structure that implements data callbacks using the Observer pattern.
- Its core structure (illustrated in the diagram below) varies slightly depending on interpretation — adapt it according to your own business context.
Creating a ViewModel
is simple. Following Google’s official recommendation, it should be instantiated with lifecycle awareness using a factory:
Local lifecycle
private val viewModel: MyViewModel by lazy {
ViewModelProvider(
this,
MyViewModelFactory()
)[MyViewModel::class.java]
}
Parent lifecycle
private val parentViewModel: RouteViewModel? by lazy {
parentFragment?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[RouteViewModel::class.java]
}
}
private val activityViewModel: ActivityViewModel? by lazy {
activity?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[ActivityViewModel::class.java]
}
}
ViewModel implementation with LiveData
class MyViewModel(
private val repository: Repository,
): ViewModel() {
private val _data: MutableLiveData<String> = MutableLiveData()
val data: LiveData<String> = _data
private val _errorMessage: MutableLiveData<Throwable> = MutableLiveData()
val errorMessage: LiveData<Throwable> = _errorMessage
private val _isRefresh: MutableLiveData<Boolean> = MutableLiveData()
val isRefresh: LiveData<Boolean> = _isRefresh
fun fetchData() {
_isRefresh.value = true
viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable ->
_errorMessage.value = throwable
_isRefresh.value = false
}) {
_data.value = repository.fetchData()
_isRefresh.value = false
}
}
}
class MyFragment: Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isRefresh.observe(this) {
showHideSpinner(it)
}
viewModel.errorMessage.observe(this) {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}
viewModel.data.observe(this) {
updateUI(it)
}
viewModel.fetchData()
}
private fun updateUI(data: String) {
println(data)
}
}
Key Points
- Unlike MVP, MVVM doesn’t require strict two-way binding. Google actually recommends one-way data flow, since DataBinding’s two-way binding is quite limited.
- With lifecycle-aware tools like LiveData, it’s easy to propagate updates from the
ViewModel
to theView
. - For single-Activity, multi-Fragment architectures, using the
parentFragment
lifecycle to handle navigation is a great approach. - One downside: each
LiveData
often represents one state (loading, success, error), which can lead to the so-called LiveData explosion.
MVI
- MVI can be seen as a refined evolution of MVVM.
- To avoid LiveData explosion, MVI uses a sealed class (in Kotlin) to enumerate all possible UI states of a request.
- The
View
observes a single state, and updates itself accordingly usingwhen
orswitch
statements.
Example
sealed class MyUIState {
class Success(val data: String): MyUIState()
class Failed(val throwable: Throwable): MyUIState()
object Refreshing: MyUIState()
}
class MyViewModel(
private val repository: Repository,
): ViewModel() {
private val _uiState: MutableLiveData<MyUIState> = MutableLiveData()
val uiState: LiveData<MyUIState> = _uiState
fun fetchData() {
_uiState.value = MyUIState.Refreshing
viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable ->
_uiState.value = MyUIState.Failed(throwable)
}) {
_uiState.value = MyUIState.Success(repository.fetchData())
}
}
}
class MyFragment: Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.uiState.observe(this) {
when (it) {
is MyUIState.Success -> {
showHideSpinner(false)
updateUI(it.data)
}
is MyUIState.Failed -> {
showHideSpinner(false)
Toast.makeText(context, it.throwable.message, Toast.LENGTH_SHORT).show()
}
is MyUIState.Refreshing -> {
showHideSpinner(true)
}
}
}
}
private fun updateUI(data: String) {
println(data)
}
}
In this structure:
- The Repository produces
UIState
, - The ViewModel handles threading and dispatch,
- The View simply renders the corresponding state.
📈 Comparison Table
Pattern | Core Components | Responsibility Division | Data Flow Direction |
---|---|---|---|
MVP (Model–View–Presenter) | Model, View, Presenter | View handles UI; Presenter handles logic; Model provides data | Two-way (Presenter ↔ View) |
MVVM (Model–View–ViewModel) | Model, View, ViewModel | ViewModel exposes observable data (LiveData, StateFlow, etc.); View observes | One-way binding (data changes drive UI) |
MVI (Model–View–Intent) | Model, View, Intent (or State) | Everything is an event; state is immutable; View renders based on state | Strict unidirectional data flow |
🧐 Analysis
Feature | MVP | MVVM | MVI |
---|---|---|---|
Data Flow | Two-way | One-way (View observes ViewModel) | Strictly unidirectional |
State Management | Scattered in Presenter | Managed by ViewModel | Centralized immutable state |
Testability | Good | Better (less UI coupling) | Best (pure functional flow) |
Complexity | Medium | Medium-high | High (but highly maintainable) |
Typical Use Cases | Early Android apps | Android Jetpack | Jetpack Compose, React, Flutter |
Additional Notes
Generally, a robust MVVM architecture can be organized into five layers:
View – The imperative UI components (
Activity
,Fragment
, XML layouts) that render data viaUIState
.ViewModel – Handles threading and exposes
UIState
viaLiveData
orFlow
.Repository – Constructs the
UIState
from data models.- This layer can be the most interesting one — it requires some discrete math thinking (e.g., proper set mappings) to correctly assemble data.
- Most of the other layers are boilerplate and can be abstracted using AOP (Aspect-Oriented Programming).
UseCase – Defines different data sources (local DB, remote API, etc.).
Service – The raw data providers.
If there is only one data source, you can skip the UseCase layer for simplicity — this should be decided case by case.