MVVM vs MVP vs MVI: Which Android Architecture Should You Choose?
The Problem
I inherited an Android project that used MVP. The team wanted to modernize, so we tried switching to MVVM. But our implementation was half-MVP, half-MVVM. The code became a mess of Presenters that called ViewModel methods, and Views that observed LiveData while also implementing Presenter interfaces.
Six months later, we had:
- Tests that mocked both Presenters and ViewModels
- Lifecycle bugs where Presenters leaked memory
- A confused team that didn’t understand which pattern to follow
I realized we chose MVVM because “Google recommends it” without understanding what it actually solved. The real question isn’t which pattern is better, but which pattern fits your project.
Understanding the Patterns
Before choosing, I needed to understand what each pattern actually does:
┌─────────────────────────────────────────────────────────────────────────────┐│ ARCHITECTURE PATTERNS │├─────────────────────────────────────────────────────────────────────────────┤│ ││ MVVM MVP MVI ││ ┌─────┐ ┌─────┐ ┌─────┐ ││ │View │◄────binding────► │View │◄────explicit────► │View │◄────state───││ └─────┘ └─────┘ contract └─────┘ ││ │ │ │ ││ ▼ ▼ │ ││ ┌─────────┐ ┌─────────┐ │ ││ │ViewModel│ │Presenter│ │ ││ └─────────┘ └─────────┘ ▼ ││ │ │ ┌──────────┐ ││ ▼ ▼ │ Intent │ ││ ┌─────┐ ┌─────┐ └──────────┘ ││ │Model│ │Model│ │ ││ └─────┘ └─────┘ ▼ ││ ┌──────────┐ ││ │ViewModel │ ││ └──────────┘ ││ │ ││ ▼ ││ ┌──────────┐ ││ │ State │ ││ └──────────┘ ││ ││ Data binding Explicit contract Unidirectional flow ││ Reactive Manual updates Predictable state ││ Lifecycle-aware Memory leaks risk More boilerplate │├─────────────────────────────────────────────────────────────────────────────┤│ ││ WHEN TO USE: ││ MVVM: New projects, Jetpack integration, reactive style ││ MVP: Legacy projects, simpler apps, non-reactive teams ││ MVI: Complex state, strict predictability, state-heavy UIs ││ │└─────────────────────────────────────────────────────────────────────────────┘MVVM: The Jetpack-Friendly Option
MVVM separates View from business logic through a ViewModel. The View observes data through LiveData or StateFlow:
class UserViewModel(private val repository: UserRepository) : ViewModel() { private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users
fun loadUsers() { viewModelScope.launch { _users.value = repository.getUsers() } }}
// Activity/Fragment observes LiveDataviewModel.users.observe(this) { userList -> adapter.submitList(userList)}The key insight: View never calls ViewModel methods directly for data. It observes. This reactive approach eliminates manual lifecycle management.
MVP: The Explicit Contract
MVP uses an explicit contract between View and Presenter. The View calls Presenter methods, Presenter calls View methods:
interface UserContract { interface View { fun showUsers(users: List<User>) fun showError(message: String) } interface Presenter { fun loadUsers() fun onDestroy() }}
class UserPresenter( private val view: View, private val repository: UserRepository) : Presenter { override fun loadUsers() { repository.getUsers { users -> view.showUsers(users) } }
override fun onDestroy() { // Manual cleanup required }}The key insight: Everything is explicit. View tells Presenter what happened. Presenter tells View what to show. No magic.
MVI: The State Machine
MVI treats UI as a state machine. User actions become Intents, which produce new States:
sealed class UserIntent { object LoadUsers : UserIntent() data class FilterUsers(val query: String) : UserIntent()}
data class UserState( val users: List<User> = emptyList(), val loading: Boolean = false, val error: String? = null)
class UserViewModel : ViewModel() { private val _state = MutableStateFlow(UserState()) val state: StateFlow<UserState> = _state.asStateFlow()
fun handleIntent(intent: UserIntent) { when (intent) { is UserIntent.LoadUsers -> loadUsers() is UserIntent.FilterUsers -> filterUsers(intent.query) } }
private fun loadUsers() { _state.update { it.copy(loading = true) } // ... load and update state }}The key insight: State flows in one direction. Intent → ViewModel → State → View. Predictable and testable.
My Trial-and-Error Process
Attempt 1: Copy Google’s MVVM Example
I copied Google’s architecture sample. But my project didn’t use data binding. My Views manually called ViewModel methods:
// WRONG: Calling ViewModel methods instead of observingclass UserActivity : AppCompatActivity() { private lateinit var viewModel: UserViewModel
override fun onCreate(savedInstanceState: Bundle?) { viewModel.loadUsers() // This is MVP-style calling!
viewModel.users.observe(this) { users -> // This is MVVM-style observing! showUsers(users) } }}Result: Hybrid code that confused everyone.
Attempt 2: Keep MVP, Add ViewModel
I kept the Presenter but wrapped it in a ViewModel:
class UserViewModel(private val presenter: UserPresenter) : ViewModel() { fun loadUsers() = presenter.loadUsers()}
// Presenter still did all the work// ViewModel was just a wrapperResult: More complexity, no benefit. Tests now mocked both Presenter and ViewModel.
Attempt 3: Proper MVVM Implementation
I removed Presenters entirely and implemented proper reactive MVVM:
BEFORE (MVP): Activity → Presenter.loadUsers() → Presenter.showUsers() → Activity.updateUI()
AFTER (MVVM): Activity observes users → ViewModel loads users → LiveData emits → Activity updatesThe Activity never calls ViewModel methods for data. It only triggers actions:
class UserActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // Trigger action viewModel.loadUsers()
// Observe result (reactive) viewModel.users.observe(this) { users -> adapter.submitList(users) }
viewModel.error.observe(this) { message -> showError(message) } }}Result: Clean separation, lifecycle handled automatically.
When Each Pattern Wins
Choose MVVM When:
- Starting a new project: Jetpack components integrate naturally
- Team is familiar with reactive patterns: LiveData, StateFlow, RxJava
- Need lifecycle-safe data: ViewModel survives configuration changes
- Want less boilerplate: No Presenter interfaces, no View callbacks
✓ New Android project✓ Using Jetpack (ViewModel, LiveData, StateFlow)✓ Team comfortable with reactive programming✓ Want automatic lifecycle handling✓ Prefer less explicit contractsChoose MVP When:
- Legacy project already uses it: Migration cost outweighs benefits
- Team unfamiliar with reactive patterns: Explicit contracts are easier to learn
- Simple UI with few state changes: Reactive overhead unnecessary
- Need strict separation: Contracts make dependencies explicit
✓ Legacy project with existing MVP✓ Team prefers explicit contracts✓ Simple UI, minimal state✓ Non-reactive development style✓ Need clear View-Presenter boundaryChoose MVI When:
- Complex UI state: Multiple states per screen, state transitions
- Need predictable state changes: Debugging state bugs is critical
- Using Compose heavily: State-driven UI matches Compose philosophy
- Want strict unidirectional flow: Every change traces through Intent → State
✓ Complex state management✓ Multiple state transitions per action✓ Heavy Jetpack Compose usage✓ Debugging state issues frequently✓ Team willing to learn pattern overheadCommon Mistakes I Made
Mistake 1: Choosing Pattern by Popularity
“Google recommends MVVM” became my default. But our project had:
- No reactive infrastructure
- Team unfamiliar with LiveData
- Simple UI with few states
MVP would have been more appropriate.
Mistake 2: Mixing Patterns
When migrating, I kept some MVP code while adding MVVM:
┌─────────────────────────────────────────────────────────────────┐│ Mixed Architecture │├─────────────────────────────────────────────────────────────────┤│ ││ Screen A: MVVM (LiveData, ViewModel) ││ Screen B: MVP (Presenter, Contract) ││ Screen C: Both (ViewModel wrapping Presenter) ││ ││ Result: ││ - Different testing strategies per screen ││ - Team confused about which pattern to use ││ - No consistent architecture ││ │└─────────────────────────────────────────────────────────────────┘Consistency matters more than choosing the “best” pattern.
Mistake 3: Over-engineering with MVI
I tried MVI on a simple login screen:
data class LoginState( val username: String = "", val password: String = "", val loading: Boolean = false, val error: String? = null, val usernameError: String? = null, val passwordError: String? = null, val formValid: Boolean = false)
sealed class LoginIntent { data class UsernameChanged(val value: String) : LoginIntent() data class PasswordChanged(val value: String) : LoginIntent() object Submit : LoginIntent()}For a simple two-field form, this added 50+ lines of boilerplate. MVVM with LiveData would have been simpler.
Practical Decision Framework
When I face a new project or migration, I use this framework:
┌───────────────────────────────────────────────────────────────────────┐│ START: New Project? ││ │ ││ ▼ ││ ┌─────────────────┐ ││ │ Using Jetpack? │ ││ └─────────────────┐ ││ YES │ NO ││ ▼ ││ ┌──────────────────────┐ ││ │ Team knows reactive? │ ││ └──────────────────────┐ ││ YES │ NO │ ││ ▼ │ ││ ┌────────────────┐ │ ││ │ MVVM │ │ ││ └────────────────┘ │ ││ │ ▼ ││ │ ┌────────────────┐ ││ │ │ MVP │ ││ │ └────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────────┐ ││ │ Complex UI state? │ ││ └──────────────────────┐ ││ YES │ NO ││ ▼ ││ ┌────────────────┐ ││ │ Consider MVI │ ││ └────────────────┘ ││ │└───────────────────────────────────────────────────────────────────────┘For Legacy Projects
If I inherit a legacy project:
- Check existing pattern: Is it MVP? MVVM? No pattern?
- Assess team familiarity: Can they learn a new pattern?
- Calculate migration cost: Full rewrite vs gradual migration
- Choose consistency: Stick with existing pattern if migration cost is high
EXISTING MVP: Team unfamiliar with MVVM → Stay with MVP Team wants modernization → Gradual MVVM migration
EXISTING MVVM: Working well → Stay with MVVM State bugs frequent → Consider MVI for problematic screens
NO PATTERN: New screens → MVVM with Jetpack Team preference → MVP with explicit contractsWhat I Learned
The key lesson: No pattern is universally better. The Chinese saying from my source material captures it perfectly: traditional architectures have no absolute good or bad, only which fits or doesn’t fit.
┌──────────────────────────────────────────────────────────────────────┐│ Pattern Selection Criteria │├──────────────────────────────────────────────────────────────────────┤│ ││ PATTERN │ BEST FOR │ WATCH OUT FOR ││ ────────────┼───────────────────────┼────────────────────────────────││ MVVM │ New projects │ Don't call ViewModel methods ││ │ Jetpack integration │ for data (observe instead) ││ │ Reactive teams │ ││ │ │ ││ MVP │ Legacy projects │ Memory leaks (manual cleanup) ││ │ Simple UIs │ Lifecycle management needed ││ │ Non-reactive teams │ ││ │ │ ││ MVI │ Complex state │ Boilerplate overhead ││ │ Compose-heavy apps │ Learning curve ││ │ Predictability needed │ ││ │└──────────────────────────────────────────────────────────────────────┘My pragmatic approach now:
- New projects: Start with MVVM + Jetpack
- State complexity grows: Evaluate MVI for problematic screens
- Legacy MVP: Stay consistent unless migration benefits outweigh cost
The pattern matters less than understanding what it solves. MVVM solves lifecycle pain. MVP solves explicit separation. MVI solves state unpredictability. Choose based on your pain points.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments