返回 Android
Android
17 分钟阅读

当我们在谈MVP、MVVM、MVI的时候,我们到底在谈什么?

Presentation Layer 架构模式的演进脉络、职责边界与数据流向——从 MVC 到 MVI 的选型与实践

当我们谈论 MVP、MVVM、MVI 时,谈的不是整个应用的分层架构(Clean Architecture、DDD 等),而是 Presentation Layer Architecture Pattern——即界面展示层的职责划分与数据流动方式

这三种模式解决的是同一个问题:如何把 UI 渲染、用户交互、状态持有、业务编排从 Activity/Fragment 中剥离出来,让界面层可测试、可复用、可预测。它们与 SSOT / UDF 原则协同:Repository 持有数据的唯一权威来源,Presentation 层负责把数据转化为 UI 可消费的状态。

Android 架构模式漫画对比:MVP 双向交互、MVVM 单向观察、MVI 状态驱动


一、问题起源:为什么需要 Presentation 层架构

1.1 God Activity / God Fragment

早期 Android 开发中,Activity 或 Fragment 往往承担过多职责:

  • 布局 inflation 与 UI 刷新
  • 网络请求、数据库读写
  • 业务规则判断
  • 生命周期与配置变更处理

这导致 UI 与业务逻辑强耦合:难以单元测试、难以复用、状态散落在各处,配置变更(如屏幕旋转)时数据丢失或重复请求。

1.2 架构模式要达成的目标

目标说明
职责分离View 只负责渲染与转发用户事件,不持有业务逻辑
数据流清晰状态变化可追踪、可预测
生命周期安全异步操作不与已销毁的 View 绑定
可测试性Presentation 逻辑可在无 UI 环境下测试

1.3 演进脉络

MVC → MVP → MVVM → MVI
     (契约回调) (可观察状态) (不可变状态 + 严格 UDF)

这不是"后者完全取代前者"的线性替代关系,而是数据流约束逐步收紧的过程。许多项目 today 仍在使用 MVP;Jetpack 官方推荐 MVVM;Compose 时代 MVI 的采纳率显著上升。


二、Presentation 模式在整体架构中的位置

MVP / MVVM / MVI 只定义 View 层以上 的交互方式。完整的 Android 分层通常还包括:

┌─────────────────────────────────────────┐
│  View(Activity / Fragment / Compose)   │  ← Presentation 模式讨论的范围
│  ViewModel / Presenter                   │
├─────────────────────────────────────────┤
│  Repository(SSOT 入口,拼装 UIState)    │
├─────────────────────────────────────────┤
│  UseCase(可选,编排单一业务用例)         │
├─────────────────────────────────────────┤
│  Service(Room Dao、Retrofit、DataStore) │
└─────────────────────────────────────────┘

Repository 是数据层的 SSOT 入口,负责本地与远程数据的合并策略;ViewModel / Presenter 负责线程调度、状态暴露与事件响应;View 根据状态渲染 UI,不直接访问数据源。

若数据来源单一,UseCase 层可省略以简化开发;若业务复杂,UseCase 能将 ViewModel 从多源编排中解放出来。Repository 层的拼装逻辑(集合映射、字段裁剪)往往是业务差异最大的部分,其余层多为模板代码——这也是 AOP 或代码生成可以介入的地方。


三、MVC:Android 语境下的困境

MVC(Model–View–Controller)是最传统的分层方式。在 Android 中:

  • View:XML 布局 + 部分 View 逻辑
  • Controller:Activity / Fragment
  • Model:数据与业务逻辑

问题在于 Android 的 View 体系能力有限——动画、复杂交互、动态布局往往不得不在 Activity/Fragment 中实现,Controller 事实上兼任了 View 的职责,退化为 "Massive View Controller"。

数据获取、UI 刷新、事件处理全部堆在 Activity 中,维护成本随功能增长急剧上升。Google 在 Architecture Components 推出后,官方推荐以 MVP / MVVM 替代传统 MVC 式的 Activity 写法。


四、MVP(Model–View–Presenter)

4.1 核心组成与职责

组件职责
View被动视图,实现 IView 接口,负责 UI 渲染与用户事件转发
Presenter接收 View 事件,调用 Model/Service 获取数据,回调 View 更新 UI
Model数据源(网络、数据库等)

4.2 数据流向

MVP 是双向数据流:View 调用 Presenter 方法触发业务;Presenter 通过 View 接口回调更新 UI。

View ──(用户事件)──→ Presenter ──(请求)──→ Model/Service
View ←──(回调更新 UI)── Presenter ←──(数据)── Model/Service

实现上通常通过 Contract 接口IView + IPresenter)定义契约,Presenter 持有 View 的弱引用,在 attachView / detachView 中绑定生命周期。

4.3 优势

  • 职责边界清晰,View 与业务逻辑解耦
  • Presenter 可独立单元测试(Mock View 接口)
  • 不依赖 Android Framework,Presenter 纯 Java/Kotlin

4.4 局限与常见误区

Presenter 过大:常被归因于 MVP 本身,实则是数据层未细分的问题。Repository、UseCase 的划分在 MVVM 中同样必要——Presenter 不应直接拼装复杂 UIState,这一职责应下沉至 Repository。

生命周期绑定:MVP 的核心痛点。若封装简陋——只实现数据回调而忽略 detachView——Presenter 的异步请求会在 View 销毁后继续回调,导致 NPE、内存泄漏甚至 ANR。这不是模式缺陷,而是实现质量差异

接口样板代码:Java 时代 Contract 接口较多,链路追踪需在多个文件间跳转,开发体验偏笨重。

4.5 与 Jetpack ViewModel 的关系

Google 推出 ViewModel 的动机之一,正是强制注入生命周期——ViewModel 在配置变更时存活、在 Fragment/Activity 真正销毁时清除,避免手动 attach/detach 的不一致。MVP 并未被"淘汰",但在 Android 生态中,新项目更常直接采用 MVVM。

MVP 契约与 Presenter 的完整示例见 Android 开发中常见的 MVP 封装形式


五、MVVM(Model–View–ViewModel)

5.1 核心组成与职责

组件职责
ViewActivity / Fragment / Compose,观察 ViewModel 状态并渲染
ViewModel持有 UI 状态,响应用户事件,调度 Repository,不持有 View 引用
ModelRepository + 数据源

5.2 数据流向:观察者模式驱动的单向绑定

MVVM 的核心变化:ViewModel 不再回调 View,而是暴露可观察的状态(LiveData、StateFlow),View 订阅状态变化后自行更新 UI。

View ──(事件)──→ ViewModel ──(请求)──→ Repository ──→ Service
View ←──(观察状态)── ViewModel ←──(数据)── Repository

这与 MVP 的双向回调有本质区别——ViewModel 对 View 无感知,生命周期由 LifecycleOwner 的 observe 机制自动管理。

MVVM 架构分层示意

MVVM 漫画版示意

架构图结合个人理解绘制,不同团队对边界的划分可能有偏差——结合具体业务理解即可。

5.3 并非必须双向绑定

DataBinding 的双向绑定(@={viewModel.field})能力有限,Google 更推荐 SSOT + 单向观察:ViewModel 暴露只读状态,View 通过 observe / collect 响应变化。这与 SSOT 指南 中的 UDF 流向一致。

5.4 ViewModel 生命周期

ViewModel 由 ViewModelProvider 创建,作用域可绑定 Fragment、Activity 或 Navigation Graph:

// Fragment 作用域
private val viewModel: MyViewModel by viewModel()
 
// Activity 作用域(单 Activity 多 Fragment 路由场景常用)
private val activityViewModel: ActivityViewModel by activityViewModels()

ViewModel 在配置变更(屏幕旋转)时保留,避免重复请求;仅在 Owner 真正销毁时调用 onCleared()。更多创建方式与 Factory 注入见 如何在 Android 开发中实现 MVVM

5.5 典型实现与 LiveData 爆炸

class MyViewModel(
    private val repository: Repository,
) : ViewModel() {
    private val _data = MutableLiveData<String>()
    val data: LiveData<String> = _data
 
    private val _errorMessage = MutableLiveData<Throwable>()
    val errorMessage: LiveData<Throwable> = _errorMessage
 
    private val _isRefresh = MutableLiveData<Boolean>()
    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
        }
    }
}

按"一个状态一个 LiveData"的写法,ViewModel 中会出现 _data_errorMessage_isRefresh 等多个可观察对象——即 LiveData 爆炸。Fragment 需分别 observe 三个 LiveData,状态组合逻辑分散,难以保证 UI 一致性。

5.6 现代 MVVM:StateFlow 与 Compose

Jetpack Compose 时代,更推荐 StateFlow<UiState> 替代多个 LiveData:

  • 单一状态容器,UI 一致性更好
  • 与 Compose 的 collectAsStateWithLifecycle() 天然契合
  • 冷流 / 热流语义更清晰

单 Activity 多 Fragment 模式下,通过 parentFragmentactivity 作用域共享 ViewModel,是常见的路由状态共享方案。


六、MVI(Model–View–Intent)

6.1 核心命题

MVI 不是"稍微优化的 MVVM",而是对 Unidirectional Data Flow(UDF) 的严格实现:

  • Intent:用户意图或系统事件(点击、下拉刷新、推送到达)
  • Model:Reducer 处理 Intent,产出新 State
  • State:不可变的 UI 状态快照,View 纯函数式渲染
  • ViewState → UI,不持有可变状态
View ──(Intent)──→ ViewModel ──(调用)──→ Repository
View ←──(State)──── ViewModel ←──(数据)── Repository
         ↑                                    │
         └──────── 单向循环,State 不可变 ──────┘

6.2 与 MVVM 的关键差异

维度MVVM(典型)MVI
状态形态多个 LiveData / 分散字段单一 UiState 不可变对象
状态更新各字段独立赋值整体替换或 copy 生成新 State
可预测性中等(多 LiveData 组合)高(任意时刻 State 可完整描述 UI)
调试需追踪多个观察者可记录 State 序列,支持 Time Travel

6.3 用密封类收敛 UIState

将 loading、success、error 合并为一个 UiState,解决 LiveData 爆炸:

sealed class MyUiState {
    data object Refreshing : MyUiState()
    data class Success(val data: String) : MyUiState()
    data class Failed(val throwable: Throwable) : MyUiState()
}
 
class MyViewModel(
    private val repository: Repository,
) : ViewModel() {
    private val _uiState = MutableLiveData<MyUiState>()
    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())
        }
    }
}

View 侧只需 observe 一个 uiState,用 when 分支渲染:

viewModel.uiState.observe(viewLifecycleOwner) { state ->
    when (state) {
        is MyUiState.Refreshing -> showHideSpinner(true)
        is MyUiState.Success -> {
            showHideSpinner(false)
            updateUI(state.data)
        }
        is MyUiState.Failed -> {
            showHideSpinner(false)
            Toast.makeText(context, state.throwable.message, Toast.LENGTH_SHORT).show()
        }
    }
}

6.4 State 与 Event 的区分

MVI 实践中需区分两类输出:

类型特征示例处理方式
State持久、可重放、描述 UI 快照列表数据、loading 标志StateFlow / LiveData 持续观察
Event一次性、消费后失效Toast、Snackbar、导航跳转SharedFlow / Channel,避免旋转屏重复触发

若把 Toast 写入 State,配置变更后 State 重放会导致 Toast 再次弹出——这是 MVI 落地时最常见的坑之一。

6.5 职责分层建议

  • Repository:拼装 Domain Model → UiState 所需的数据结构
  • ViewModel:线程调度、Intent 处理、State 转换
  • View:纯渲染,State → UI,不含业务判断

七、三种模式综合对比

7.1 架构维度

模式核心组成职责划分数据流动
MVPModel、View、PresenterView 展示,Presenter 编排,Model 供数双向(Presenter ↔ View 互相调用)
MVVMModel、View、ViewModelViewModel 暴露可观察状态,View 只订阅单向观察(状态驱动 UI)
MVIModel、View、Intent/State交互即 Intent,State 不可变,View 纯渲染严格 UDF(Intent → State → View)

7.2 工程维度

特性MVPMVVMMVI
学习曲线中高
样板代码接口较多(Contract)中等State/Intent 定义增加
状态管理分散在 PresenterViewModel 多 LiveData 或 StateFlow中心化不可变 State
可测试性较好(Mock View)更好(无 View 依赖)最好(纯函数式 State 转换)
生命周期手动 attach/detachViewModel 自动管理同 MVVM
典型场景遗留项目、Java 代码库Jetpack 官方推荐、XML + FragmentCompose、复杂状态页面

7.3 数据流对比(一图胜千言)

MVP:     View ←────────→ Presenter → Model

MVVM:    View ──→ ViewModel → Repository
         View ←── observe ── ViewModel

MVI:     View ──→ Intent ──→ ViewModel → Repository
         View ←── State ──── ViewModel
              (immutable, single source)

八、选型建议

8.1 按项目阶段

阶段建议理由
遗留 Java 项目维护继续 MVP 或渐进迁移 MVVM重写成本高,Presenter 已有投资
新 Fragment + XML 项目MVVM + StateFlowJetpack 生态成熟,官方文档完善
新 Compose 项目MVVM 或 MVICompose 天然适配单一 State 驱动
状态复杂的单页(编辑器、表单 wizard)MVI不可变 State 便于调试与回溯

8.2 按团队能力

  • 小团队 / 快速交付:MVVM + 单一 UiState 密封类(MVI 轻量版),不必引入完整 Reducer 框架
  • 大团队 / 多端一致:严格 MVI + 状态日志,便于 Code Review 与问题复现
  • 测试要求高:优先 MVI 或 MVVM + StateFlow,ViewModel 测试覆盖 State 转换

8.3 常见误区

  1. 把 MVVM 等同于 DataBinding 双向绑定——单向观察 + SSOT 才是 Google 推荐路径
  2. 把 MVI 等同于"用一个 sealed class 包 LiveData"——完整 MVI 还需区分 State 与 Event、保证 State 不可变
  3. 在 View 中直接调用 Repository——破坏分层,无论哪种模式都应禁止
  4. Presenter / ViewModel 中写 UI 逻辑(如 Toast.makeText)——应通过 State/Event 通知 View 执行

九、总结

MVP、MVVM、MVI 讨论的是 Presentation Layer 如何组织代码与数据流,而非银弹式的架构替换。

  • MVP 用契约接口实现 View 与 Presenter 解耦,生命周期需开发者自行保障
  • MVVM 用可观察状态替代回调,ViewModel 自动扛配置变更,是 Jetpack 官方主线
  • MVI 在 MVVM 基础上收紧为严格 UDF,用不可变 State 换取可预测性与可调试性

三者共享同一目标:View 只管渲染,状态有唯一出口,数据流可追溯。选型时不必教条——许多成熟项目采用 "MVVM + UiState 密封类 + Event Channel" 的混合方案,已能覆盖大部分场景。关键是在团队内达成一致的职责边界与状态管理约定,并与 Repository 层的 SSOT 原则对齐。

相关文章

Android
6 分钟
Android开发中常见的MVP封装形式
缩略版 MVP 封装——契约接口 + Presenter 拉数据,搞懂它更好理解 MVVM。
Android
4 分钟
如何在Android开发中实现MVVM的架构
MVVM与Android的实践简要指导
Android
26 分钟
Android 架构核心原则:单一数据源(SSOT)与单向数据流(UDF)实战指南
在Android开发中,随着应用复杂度提升,数据混乱、状态不一致、调试困难等问题频发,而单一数据源(Single Source of Truth, SSOT)与单向数据流(Unidirectional Data Flow, UDF)正是解决这些痛点的核心架构原则。