Mobile Architecture Patterns
MVC, MVP, MVVM, and MVI: how each pattern separates concerns on mobile, their testability characteristics, and how they relate to declarative UI frameworks like Jetpack Compose and SwiftUI.
What it is
Mobile architecture patterns are structural conventions for separating presentation logic, business logic, and data access in mobile applications. The dominant patterns — MVC, MVP, MVVM, and MVI — differ primarily in where they place the presentation logic, how they handle data binding between UI state and rendered views, and how they manage the unidirectional vs bidirectional flow of data. The choice of pattern has profound implications for testability (unit-testing UI logic without the Android or iOS frameworks), maintainability (isolating concerns so that changes in one layer do not ripple through others), and compatibility with modern declarative UI frameworks.
The evolution from MVC to MVI reflects the industry's experience with the unique constraints of mobile: tight coupling between UI lifecycle and business logic, the difficulty of testing code that depends on platform frameworks, the complexity of managing state across screen transitions, and the need to handle asynchronous data sources (network, database, sensor streams) cleanly.
Why it exists
Early Android development used MVC implicitly, with Activities (God objects that mixed business logic, UI manipulation, and lifecycle management) serving as controller, view, and model simultaneously. This created Activities with thousands of lines of code that were impossible to test without running on a device or emulator. iOS UIKit development had similar problems with Massive View Controllers. The community developed MVP, MVVM, and MVI as responses to this structural problem — each providing a way to extract platform-independent logic into classes that can be tested in pure JVM or Swift unit tests.
When to use
- MVI (Model-View-Intent): Complex screens with many state transitions, teams using Jetpack Compose or SwiftUI, or any application where state management complexity is the primary concern.
- MVVM: Most modern Android and iOS applications; the default choice when using Android Architecture Components (ViewModel, LiveData/StateFlow) or SwiftUI with
@ObservableObject. - MVP: Teams maintaining existing MVP codebases; legacy UI toolkit code (UIKit, Android XML views) where data binding is not available or practical.
- MVC: Small applications or prototypes where the overhead of full pattern adoption is not justified.
When not to use
- Do not apply heavyweight patterns to trivial screens — a settings screen that toggles a single boolean does not require a full Clean Architecture stack.
- Avoid mixing patterns within a single codebase without a clear migration strategy — inconsistency across screens creates confusion and reduces the maintenance benefits of any single pattern.
- Do not use MVC on modern Android/iOS where lifecycle complexity requires the ViewModel separation that MVVM provides.
Typical architecture
MVC (Model-View-Controller)
────────────────────────────
Model ←── Controller ──→ View
Controller updates View directly
View may update Controller (user events)
Problem: Controller knows about View (tight coupling); hard to test
MVP (Model-View-Presenter)
───────────────────────────
Model ←── Presenter ──→ View (interface)
View is a thin interface; Presenter does all logic
+ Presenter is fully testable (no platform dependency)
- Lots of boilerplate (View interfaces for every screen)
- Two-way coupling: Presenter holds View reference
MVVM (Model-View-ViewModel)
────────────────────────────
Model ←── ViewModel ──→ State (observable)
↑ ↓
View ────── binds to State
ViewModel exposes observable state (LiveData, StateFlow, @Published)
View observes and re-renders on state change
+ ViewModel has no reference to View (no memory leak risk)
+ Lifecycle-safe (ViewModel survives config changes on Android)
+ Excellent testability
MVI (Model-View-Intent) — Unidirectional Data Flow
────────────────────────────────────────────────────
View ──→ Intent (user action) ──→ Reducer/ViewModel
↑ ↓
└──────── State ←──────────┘
Single immutable state object; state = f(state, intent)
+ Predictable: one source of truth for all UI state
+ Debuggable: log every intent+state transition → reproduce bugs
+ Perfect fit for Compose/SwiftUI (declarative render from state)
- More boilerplate than MVVM for simple screens
- Learning curve for teams new to functional state management
CLEAN ARCHITECTURE ON MOBILE
──────────────────────────────
Presentation (MVVM/MVI ViewModel)
↓ UseCase/Interactor
Domain (business logic, no platform deps)
↓ Repository interface
Data (Repository impl, local DB, remote API)
+ Domain layer is pure Kotlin/Swift — fast unit tests
- Significant boilerplate; best justified for large teams
Pros and cons
Pros
- MVVM/MVI separate business logic from platform framework, enabling fast JVM unit tests without emulator.
- MVI's single immutable state object eliminates an entire class of bugs related to inconsistent UI state.
- ViewModel (Android) survives configuration changes — no need to re-fetch data on screen rotation.
- Repository pattern provides a clean abstraction over data sources — swap network for cache without changing ViewModels.
- MVI's intent log enables time-travel debugging and exact bug reproduction from production crash reports.
Cons
- MVVM and especially MVI require significantly more code than vanilla MVC for simple screens.
- Clean Architecture adds additional layers (Use Cases) that may be unnecessary for smaller teams or simpler apps.
- MVI's sealed class intent hierarchies can become large and unwieldy on complex screens with many user actions.
- Data binding (MVVM on Android with XML) has a steep learning curve and complex debugging when bindings fail silently.
Implementation notes
MVVM with Kotlin Coroutines and StateFlow (Android): The ViewModel exposes a single StateFlow<ScreenState> that the composable or fragment collects. All state mutations happen inside the ViewModel via coroutines launched in viewModelScope. The ViewModel holds no reference to any Android framework class except Application (if needed) — this makes it unit-testable. The repository abstracts Retrofit (remote) and Room (local); the ViewModel calls the repository and transforms the result into screen state.
MVI with Jetpack Compose: The ViewModel receives a sealed class Intent (user actions), processes them via a reduce(state, intent) → state function, and emits the new state to a StateFlow. The Compose UI reads from the state flow and maps it to composables; user events are expressed as intent objects passed to the ViewModel. The composable is a pure function of state — given the same state, it produces the same UI. Side effects (navigation, toasts) are emitted separately via a Channel<Effect> or SharedFlow<Effect>, consumed once by the UI.
Common failure modes
- Massive ViewModel: Moving logic from Activity/Fragment to ViewModel without further separation produces Massive ViewModels — the same antipattern as Massive View Controllers, just moved one layer over.
- Leaking Android context into ViewModel: Holding a reference to an Activity or Fragment in the ViewModel causes memory leaks across configuration changes; use
AndroidViewModelonly whenApplicationcontext is strictly necessary. - MVI intent explosion: Defining a separate intent for every button click on a complex screen produces enormous sealed classes; group related intents into sub-sealed classes per feature area.
- Repository not cached: The repository fetches from network every time the ViewModel calls it — no local caching — leading to slow screens on poor connectivity and unnecessary API cost.
Decision checklist
- Is the ViewModel free of direct references to Android/iOS UI framework classes (Activity, Fragment, UIViewController)?
- Can the ViewModel be unit-tested without a running emulator or simulator?
- Is all UI state contained in a single observable state object rather than scattered across multiple LiveData/StateFlow fields?
- Does the repository abstract local and remote data sources so the ViewModel does not know which is used?
- Are side effects (navigation, toasts) separated from state emissions?
Example use cases
- E-commerce app product listing: MVI ViewModel receives LoadProducts, FilterByCategory, and AddToCart intents; emits a single ProductListState containing items, filters, loading/error flags, and cart count — the Compose UI is a pure rendering of that state.
- Banking app with biometric auth: Clean Architecture with MVVM: presentation layer ViewModel calls an AuthenticateUserUseCase in the domain layer, which calls a BiometricRepository; no platform framework code exists in the domain layer, enabling 100% unit test coverage of authentication logic.
Related patterns
- Offline-First Design — The repository pattern in MVVM/MVI is the natural integration point for offline-first sync logic.
- Mobile Security — Secure storage and biometric authentication integrate into the data layer of MVVM/Clean Architecture.
- Mobile Performance — ViewModel lifecycle awareness is essential for avoiding redundant network calls that degrade performance and battery life.