跪拜 Guibai
← Back to the summary

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

MVC architecture diagram

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

Disadvantages

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

MVP architecture diagram

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

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

MVVM architecture diagram

Image: MVVM architecture (View observes state exposed by ViewModel)

Core principles

Advantages

Challenges

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.png 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.png

Image: UI layer unidirectional data flow (UDF) (source: developer.android.com [3])

Core principles

Workflow

  1. Data layer provides app data to ViewModel.
  2. ViewModel processes data and exposes it as UI State to the UI.
  3. UI elements read state and render.
  4. 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() / repeatOnLifecycle to 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.

Compose state hoisting diagram

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:

// 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

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]

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

13.3 Modern Android Modularization

Modularization supports parallel development across teams, improves build speed, and enforces module boundaries.

Modularization diagram

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

ByteDance / Alibaba (⚠️ Based on search summaries, not verified against original sources)


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

  1. Introduce ViewModel: have Presenter hold a ViewModel, or directly change Presenter into ViewModel, letting it handle lifecycle.
  2. Define UiState: merge scattered View interface callbacks (e.g., showLoading, showData) into one immutable state data class.
  3. Change state exposure: expose UiState via StateFlow (or LiveData); View layer subscribes to state.
  4. Gradually remove Contract: delete Contract classes once View interfaces are replaced by state subscriptions and Presenter interfaces by ViewModel methods.
  5. 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

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

  1. developer.android.com - Guide to app architecture
  2. developer.android.com - Recommendations for Android architecture
  3. developer.android.com - UI layer
  4. developer.android.com - UI layer events
  5. developer.android.com - Domain layer
  6. developer.android.com - Data layer
  7. developer.android.com - State hoisting in Compose
  8. developer.android.com - Dependency injection with Hilt
  9. github.com - android/nowinandroid - Architecture Learning Journey
  10. github.com - android/architecture-samples
  11. github.com - android/sunflower
  12. blog.cleancoder.com - The Clean Architecture (2012-08-13)
  13. tech.meituan.com - How to build Android MVVM application framework (2016-11-11)
  14. tech.meituan.com - Meituan Waimai Android platform architecture evolution (2018-03-16)
  15. tech.meituan.com - WMRouter: Meituan Waimai Android multi-app reuse practice (2018-09-27)
  16. medium.com - Airbnb Tech - Introducing Trio: Part I (2024-03)
  17. github.com - airbnb/mavericks
  18. square.github.io - Workflow Concepts
  19. hannesdorfmann.com - Mosby: A Model-View-Presenter library
  20. proandroiddev.com - Google's UDF vs real world MVI-flavored MVVM
  21. infoq.com - Android Jetpack Navigation 3 Overview (2025-11)
  22. github.com - Uber RIBs
  23. slackhq.github.io - Slack Circuit
  24. proandroiddev.com - Get your MVP right with Mosby (2017-05-15)
  25. developer.squareup.com - Why Workflow: Scaling Square Point of Sale