The Road to State-Driven Android: From MVC to MVI-Flavored MVVM
1. Core Conclusions
| Conclusion | Explanation |
|---|---|
| Official architecture guidance | Google does not mandate a specific pattern (MVC/MVP/MVVM) but recommends layered architecture + Unidirectional Data Flow (UDF), emphasizing separation of concerns and predictable state. [1][2] |
| Mainstream industry practice | MVVM + a single UiState (incorporating MVI ideas) is currently the most common UI architecture and handles complex state synchronization well. [20] |
| Recommended tech stack | UI: Compose; State: StateFlow; DI: Hilt (officially recommended, not mandatory); Data: Repository. [2][8] |
| Navigation evolution | Navigation 3 hands the back stack to developers as state, making navigation state-driven and consistent with Compose's declarative style. [21] |
2. Architecture Evolution Timeline
Android app architecture has roughly gone through four phases (years are approximate ranges to illustrate trends, not strict boundaries):
| Phase | Approx. Time | Representative Pattern | Characteristics | Legacy Problems |
|---|---|---|---|---|
| Early | 2008 – 2013 | MVC | Massive Activity, view and logic tightly coupled | Hard to unit test, high maintenance cost |
| Decoupling | 2013 – 2017 | MVP | Contract interfaces separate view and logic | Interface explosion, lifecycle management still complex |
| Data-driven | 2017 – 2021 | MVVM | ViewModel + LiveData (Architecture Components released in 2017), data changes drive view updates | Multiple data sources scatter state, consistency hard to track |
| Principle-driven | 2021 – present | MVI / UDF | Single UiState centralizes page state, unidirectional data flow | Requires designing UiState and event models |
MVC ──▶ MVP ──▶ MVVM ──▶ MVVM + Single UiState (MVI flavor) / UDF
coupled interface decoupled data-driven state centralized, data flows one way
3. MVC: Principles, Pros, and Cons
Model-View-Controller
Image: MVC architecture (Activity/Fragment acts as both View and Controller)
Core pain point: In Android, Activity is responsible for both UI initialization (View) and event handling (Controller), with blurry boundaries that eventually become a Massive View Controller.
Advantages
- Simple and intuitive structure: concepts are easy to grasp, consistent with event-driven programming intuition.
- Low barrier to entry: no additional frameworks or abstraction layers needed.
Disadvantages
- Massive View Controller: business logic and UI logic are mixed in Activity, code volume is large, and maintainability suffers.
- Hard to unit test: Controller strongly depends on the Android SDK (Context, View), cannot be tested in a pure JVM environment.
- Fragile state management: configuration changes like screen rotation recreate Activity; without special handling, UI state is lost.
When to use: Not recommended for modern Android production projects; only suitable for teaching demos, minimal prototype validation (PoC), or one-off tool pages.
4. MVP: Principles and Feature Analysis
Image: MVP architecture (View and Presenter communicate through Contract interfaces)
Core advantage
Separating View and Presenter through Contract interfaces. The Presenter does not depend on the Android SDK and can be tested in pure JVM unit tests, significantly improving test efficiency and code reliability.
Main pain points
- Interface explosion: every screen needs IView/IPresenter interfaces, leading to lots of boilerplate.
- Lifecycle management: Presenter holds a View reference. Using
WeakReferenceonly allows the View to be garbage collected; ongoing network requests or coroutines must still be manually cancelled indetachView()to avoid memory leaks or callbacks to destroyed UI. [19][24]
Configuration change handling
Native MVP handles configuration changes like screen rotation poorly. The community often uses the Mosby framework, leveraging mechanisms like Fragment's setRetainInstance(true) to preserve Presenter instances. [19][24] (setRetainInstance is deprecated in newer Fragment versions; this approach is mainly seen in older projects.)
Example (Kotlin)
interface UserContract {
interface View { fun showLoading(); fun showUser(user: User); fun showError(msg: String) }
interface Presenter { fun attach(view: View); fun detach(); fun loadUser(id: String) }
}
class UserPresenter(private val repo: UserRepository) : UserContract.Presenter {
private var view: UserContract.View? = null
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun attach(view: UserContract.View) { this.view = view }
override fun detach() { view = null; scope.coroutineContext.cancelChildren() } // must cancel manually
override fun loadUser(id: String) {
view?.showLoading()
scope.launch {
runCatching { repo.getUser(id) }
.onSuccess { view?.showUser(it) }
.onFailure { view?.showError(it.message ?: "error") }
}
}
}
When to use: Legacy projects already on MVP that are not migrating yet.
5. MVVM: Principles and Analysis
Image: MVVM architecture (View observes state exposed by ViewModel)
Core principles
- ViewModel does not hold a View: communication happens via the observer pattern.
- State-driven UI: UI is a mapping of state; data changes refresh the interface automatically.
Advantages
- Data survives configuration changes: screen rotation does not lose data.
- Low risk of memory leaks: ViewModel does not reference a View and is lifecycle-aware (as long as it does not hold a View/Activity Context).
- High testability: business logic is independent of the Android framework.
Challenges
- Tedious state definition: UiState data classes require careful design.
- One-off events: events like Toast and navigation must be handled carefully. Officially, the recommendation is to model events as UI state, with the UI notifying the ViewModel to clear them; Channel and similar approaches are community practices with trade-offs. [4]
Example (Kotlin)
data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val errorMessage: String? = null
)
@HiltViewModel
class UserViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
_uiState.update { it.copy(isLoading = true) }
viewModelScope.launch {
runCatching { repo.getUser(id) }
.onSuccess { u -> _uiState.update { it.copy(isLoading = false, user = u) } }
.onFailure { e -> _uiState.update { it.copy(isLoading = false, errorMessage = e.message) } }
}
}
fun errorShown() = _uiState.update { it.copy(errorMessage = null) }
}
When to use: The mainstream choice for new projects. The official recommended pattern (ViewModel as state holder + UDF) is closest to it, and it works best with Jetpack components (StateFlow, Compose, etc.).
6. Nine-Dimension Comparison of Three Architectures
| Dimension | MVC | MVP | MVVM |
|---|---|---|---|
| 1. Core idea | View and model separated, controller coordinates | Interface-oriented programming, direct V-M connection cut | Data-driven views, reactive programming |
| 2. Responsibility division | Activity/Fragment is both V and C, can bloat | Clear; Presenter handles pure logic | Clear; ViewModel focuses on state management |
| 3. Reference relation | View depends on Model, Controller depends on both | View and Presenter hold references to each other | View depends one-way on ViewModel |
| 4. Testability | Poor (strong Android SDK dependency) | Good (can mock View interface to test P layer) | Very good (ViewModel pure logic, no View mock needed) |
| 5. Coupling | High | Medium (bound by interface contracts) | Low (loosely coupled via observer pattern) |
| 6. Boilerplate amount | Low | High (lots of View/Presenter interfaces) | Medium (UiState classes + observation code) |
| 7. Memory leak risk | High (async callbacks often hold Activity) | Relatively high (manual View unbind and task cancel needed) | Low (ViewModel doesn't hold View, lifecycle-aware) |
| 8. Configuration change support | Poor (data lost on rotation) | Poor (manual save/restore or use Mosby) | Good (ViewModel instance retained automatically) |
| 9. Learning cost | Low | Medium (understand interface isolation) | Medium-high (need coroutines/Flow and reactive programming) |
7. Choosing by Project Type
| Project type | Recommended architecture | Reason |
|---|---|---|
| New project (Compose) | MVVM + UDF | Matches declarative UI's data-driven nature. UDF makes state predictable; ViewModel manages UI state, consistent with official recommendations. |
| Legacy View project | MVP or progressive MVVM | Keep existing MVP stable, avoid large-scale refactor risk; introduce MVVM gradually in new feature modules. |
| Complex interaction / finance | MVI-flavored MVVM | When state changes frequently and consistency is critical, Intent-driven and single state source reduce state conflicts and improve robustness. |
| Very large team | MVVM + deep modularization | Combine Clean Architecture ideas, split modules by business boundary; via DI and interface isolation, support parallel development across teams. |
8. Official Architecture Guide: Layered Structure
Image: Android official recommended layered app architecture (source: developer.android.com [1])
| Layer | Responsibility | Main components |
|---|---|---|
| UI Layer | Display app data on screen, respond to user interactions | UI elements (Activity / Compose), state holders (ViewModel) |
| Domain Layer (optional) | Between UI and data layers; encapsulates complex business logic or simple logic reused by multiple ViewModels, avoids duplication [5] | UseCase / Interactor |
| Data Layer | Contains app data and business logic, follows Single Source of Truth (SSOT) principle, ensures data consistency and reliability [6] | Repository (SSOT), data sources (network / database) |
9. Official Architecture Guide: Unidirectional Data Flow (UDF)
Image: UI layer unidirectional data flow (UDF) (source: developer.android.com [3])
Core principles
- State flows down
- Events flow up
Workflow
- Data layer provides app data to ViewModel.
- ViewModel processes data and exposes it as UI State to the UI.
- UI elements read state and render.
- User interactions produce events, sent back to ViewModel, forming a loop.
Note: The official "event handling decision tree" image link is dead (404); this article replaces it with text: UI logic (e.g., expand/collapse) is handled in the UI; business logic goes to ViewModel; one-off events produced by ViewModel should preferably be modeled as UI state. [4]
10. State Holders and View Binding Comparison
10.1 State Holders
| Solution | Positioning | Characteristics | When to use |
|---|---|---|---|
| LiveData | Traditional | Lifecycle-aware; setValue only on main thread, use postValue from background; few operators |
Simple UI state observation, Java or legacy projects |
| StateFlow | Official recommendation | Must have initial value, naturally fits UI state continuity; coroutine-based, switchable Dispatcher; built-in deduplication (skips same value) | Exposing UI state (UiState) |
| SharedFlow | Event stream | No initial value; replay and buffer configurable |
Multi-subscriber event broadcast; officially not recommended for one-off UI events (like Toast) |
When collecting StateFlow in Compose or Lifecycle, use
collectAsStateWithLifecycle()/repeatOnLifecycleto avoid collecting while UI is in background.
10.2 View Binding
| Solution | Characteristics | Recommendation |
|---|---|---|
| ViewBinding (preferred) | No annotation processing, low build overhead; auto-generated binding classes, type-safe and null-safe, avoids NPE from findViewById |
First choice for View-based projects |
| DataBinding | Supports two-way binding, expressions in XML; requires annotation processing, slower builds for large projects; logic mixed in XML is hard to debug | Not recommended for new projects; not needed for Compose |
11. Compose State Hoisting and Navigation 3
11.1 State Hoisting [7]
Move state to the lowest common ancestor of the composables that use it, following UDF: state passes down as parameters, events pass up as lambdas.
Image: State down, events up state hoisting, source: slide 13 of this PPT
| State holder | Manages |
|---|---|
| Composable itself | Simple UI element state (e.g., expand/collapse) |
| Ordinary State Holder class | Complex UI logic and state |
| ViewModel | Business logic and screen-level state, longer lifecycle |
@Composable
fun UserRoute(viewModel: UserViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
UserScreen(uiState = uiState, onRetry = { viewModel.loadUser("42") })
}
@Composable
fun UserScreen(uiState: UserUiState, onRetry: () -> Unit) { /* stateless, easy to preview and test */ }
11.2 Navigation 3: State-Driven Navigation [21]
Navigation 3 replaces imperative navigation with Compose's reactive style:
- Back stack held by developer: the back stack is an observable state list; navigation is adding/removing from that list, and UI renders accordingly.
- Type-safe: routes are Kotlin types (can use Kotlin Serialization), no longer string-concatenated.
// Illustrative code: expresses "navigation as state change", exact API per official docs
@Serializable data object Home : NavKey
@Serializable data class Profile(val id: String) : NavKey
val backStack = rememberNavBackStack(Home)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<Home> { HomeScreen(onOpen = { id -> backStack.add(Profile(id)) }) }
entry<Profile> { key -> ProfileScreen(key.id) }
}
)
12. MVI and MVVM Fusion
MVI core ideas
- Single state object: UI state in one immutable data class, avoiding inconsistency across multiple state streams.
- Explicit Intent: user actions abstracted as explicit intents (Intent/Action), uniformly passed to ViewModel.
- Pure reducer function: new state computed from current state and new event via a pure function, results predictable.
What modern MVVM absorbs
Aggregating all state into a single UiState replaces multiple LiveData:
data class NewsUiState(
val isLoading: Boolean = false,
val articles: List<Article> = emptyList()
)
Using MutableStateFlow.update for atomic updates, similar to a reducer:
// thread-safe atomic update
_uiState.update { currentState ->
currentState.copy(isLoading = true)
}
Note: "UDF is essentially an MVI variant" is mainly a community view [20], not an official definition. A safer statement: the official recommended architecture (UDF + StateFlow) is an improved MVVM that absorbs MVI's strengths.
13. Clean Architecture and Modularization
13.1 Clean Architecture Core [12]
- Dependency rule: source dependencies must point inward, toward higher-level policy.
- Outer layers (UI, DB, Network) depend on inner layers (Entities, UseCases); inner layers don't know about outer layers.
- Business logic is independent of frameworks, UI, and database, facilitating testing.
Relation to official layering: in the official architecture, the domain layer is optional, with dependency direction UI → Domain → Data. This is lighter and more pragmatic than classic Clean Architecture. [1][5]
13.2 UseCase Debate and Practice
- Debate: if UseCase just forwards (ViewModel → UseCase → Repository), it becomes unnecessary boilerplate.
- Practice suggestions (reference Now in Android [9]):
- Use UseCase to encapsulate complex business logic;
- Use it to combine multiple Repositories;
- Avoid pure forwarding;
- Introduce on demand: simple CRUD calls Repository directly; complex interactions introduce UseCase.
13.3 Modern Android Modularization
Modularization supports parallel development across teams, improves build speed, and enforces module boundaries.
Image: Modern Android modular layering (:app → :feature → :core), source: slide 15 of this PPT
14. Application Examples (1): Official Samples
| Project | Positioning | Highlights |
|---|---|---|
| architecture-samples [10] | Official architecture blueprint | main branch: Compose + Hilt + Flow; views branch: XML + Navigation |
| Now in Android (NiA) [9] | Modern Android development demo | Step-by-step reactive data flow explanation in official architecture learning docs; offline-first architecture |
| Sunflower [11] | Gardening app sample | Full process migrating from View to Compose, hybrid UI architecture demo (repo archived, still useful as reference) |
15. Application Examples (2): Industry Practice
15.1 International Companies
| Company / Framework | Architecture characteristics |
|---|---|
| Airbnb Mavericks / Trio [16][17] | Mavericks is Airbnb's open-source MVI-style framework; Trio builds on it for Compose, treating navigation as part of state, unifying UI state and route state management, simplifying state synchronization on complex screens. |
| Square Workflow [18][25] | Uses a state machine tree model, abstracting app logic into composable workflows, tightly controlling state transitions. Square uses it in POS and similar business areas for high determinism and testability. |
| Uber RIBs [22] | Router, Interactor, Builder architecture. Driven by business logic rather than views, uses tree structure for business isolation, supports large team collaboration. |
| Slack Circuit [23] | Architecture framework for Compose and Coroutines, provides clean Presenter/UI contracts, representing the declarative UI era direction. |
15.2 Domestic Chinese Companies
Meituan Technology Team
- 2016, early MVVM exploration [13]: early adoption of DataBinding and MVVM in China, solving view updates for complex forms and lists, improving maintainability.
- From 2018, platform-based architecture [14][15]: after business expansion, Meituan Waimai evolved into a componentized, platform-centric architecture (e.g., WMRouter supports multi-app reuse), emphasizing underlying capability reuse and engineering efficiency.
ByteDance / Alibaba (⚠️ Based on search summaries, not verified against original sources)
- General trend moving toward MVI / UDF paradigm: standardizing data flow, reducing state competition under complex interactions;
- Treating UI as an immutable snapshot of state at a given moment, with Diff-based rendering optimization;
- Architecture thinking converging with frontend (React/Vue) and cross-platform frameworks (Flutter).
16. Best Practices: Selection Decisions and Migration
16.1 New Project Architecture Selection
| Layer | Recommendation | Explanation |
|---|---|---|
| UI | Jetpack Compose | Declarative UI |
| VM | MVVM (UDF) | Unidirectional data flow state management |
| DI | Hilt | Officially recommended DI (not mandatory) |
| Data | Repository | Single Source of Truth principle |
16.2 Five Steps to Migrate from MVP to MVVM
- Introduce ViewModel: have Presenter hold a ViewModel, or directly change Presenter into ViewModel, letting it handle lifecycle.
- Define UiState: merge scattered View interface callbacks (e.g.,
showLoading,showData) into one immutable state data class. - Change state exposure: expose UiState via StateFlow (or LiveData); View layer subscribes to state.
- Gradually remove Contract: delete Contract classes once View interfaces are replaced by state subscriptions and Presenter interfaces by ViewModel methods.
- Introduce Repository: move data-fetching logic from ViewModel to Repository, manage data sources uniformly.
Migrate page by page or feature module by module; after each migration, add ViewModel unit tests as a regression safeguard.
17. Best Practices: Tech Stack, Testing, and Anti-Patterns
17.1 Recommended Tech Stack
| Area | Recommendation |
|---|---|
| UI layer | Jetpack Compose / ViewBinding |
| State management & async | StateFlow + Coroutines |
| Dependency injection | Hilt (based on Dagger) |
| Data layer | Repository + Room + Retrofit |
| Navigation | Navigation 3 (type-safe) |
17.2 Testing Strategy
| Test target | Method |
|---|---|
| ViewModel | Focus on state assertions: after input events, check whether state exposed by StateFlow matches expectations; use Turbine to test Flow emission sequences. |
| Repository | Use Fake implementations (officially preferred) or MockK to isolate external dependencies like network/database, verify business logic and data transformations. |
| UI | Use Compose Semantics tests to check UI node existence, state, and interactions; View-based projects continue with Espresso. |
17.3 Common Anti-Patterns
| Anti-pattern | Problem | Correct approach |
|---|---|---|
| ViewModel holds View / Activity Context | ViewModel outlives Activity/Fragment, memory leak | Only expose state; use Application Context when Context is truly needed |
| UI layer accesses data sources directly | Activity calls Room/Retrofit directly, breaks layering and UDF | Go through ViewModel → Repository |
| Using SharedFlow for one-off events | Events may be lost or reprocessed on config change or when UI is not foreground | Model events as part of UI state [4] |
| Misusing GlobalScope | Cannot control coroutine lifecycle, background tasks leak | Use viewModelScope / lifecycleScope |
| Multiple scattered LiveData/StateFlow exposing same page state | Inconsistent state, hard to track | Merge into single UiState |
| UseCase only forwards | Adds boilerplate, no real value | Introduce only when logic is complex or reused |
18. Summary and Outlook
- Evolution of state management thinking: the essence of architecture evolution is not just code structure or layering changes, but an upgrade in how "state" is understood and managed: from chaos to order, from imperative to declarative.
- Clearly manage state and data flow: architecture value lies not in whether it's called MVC, MVP, MVVM, or MVI, but in whether it provides clear, predictable data flow in complex business.
- UDF and SSOT: regardless of pattern, adhering to Unidirectional Data Flow (UDF) and Single Source of Truth (SSOT) is the foundation for reducing system complexity and avoiding state inconsistency.
Final conclusion: stick to core principles, don't blindly chase pattern labels, to ensure long-term maintainability and extensibility of Android app architecture.
19. References
References
- developer.android.com - Guide to app architecture
- developer.android.com - Recommendations for Android architecture
- developer.android.com - UI layer
- developer.android.com - UI layer events
- developer.android.com - Domain layer
- developer.android.com - Data layer
- developer.android.com - State hoisting in Compose
- developer.android.com - Dependency injection with Hilt
- github.com - android/nowinandroid - Architecture Learning Journey
- github.com - android/architecture-samples
- github.com - android/sunflower
- blog.cleancoder.com - The Clean Architecture (2012-08-13)
- tech.meituan.com - How to build Android MVVM application framework (2016-11-11)
- tech.meituan.com - Meituan Waimai Android platform architecture evolution (2018-03-16)
- tech.meituan.com - WMRouter: Meituan Waimai Android multi-app reuse practice (2018-09-27)
- medium.com - Airbnb Tech - Introducing Trio: Part I (2024-03)
- github.com - airbnb/mavericks
- square.github.io - Workflow Concepts
- hannesdorfmann.com - Mosby: A Model-View-Presenter library
- proandroiddev.com - Google's UDF vs real world MVI-flavored MVVM
- infoq.com - Android Jetpack Navigation 3 Overview (2025-11)
- github.com - Uber RIBs
- slackhq.github.io - Slack Circuit
- proandroiddev.com - Get your MVP right with Mosby (2017-05-15)
- developer.squareup.com - Why Workflow: Scaling Square Point of Sale