当我们在谈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 可消费的状态。

一、问题起源:为什么需要 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 核心组成与职责
| 组件 | 职责 |
|---|---|
| View | Activity / Fragment / Compose,观察 ViewModel 状态并渲染 |
| ViewModel | 持有 UI 状态,响应用户事件,调度 Repository,不持有 View 引用 |
| Model | Repository + 数据源 |
5.2 数据流向:观察者模式驱动的单向绑定
MVVM 的核心变化:ViewModel 不再回调 View,而是暴露可观察的状态(LiveData、StateFlow),View 订阅状态变化后自行更新 UI。
View ──(事件)──→ ViewModel ──(请求)──→ Repository ──→ Service View ←──(观察状态)── ViewModel ←──(数据)── Repository
这与 MVP 的双向回调有本质区别——ViewModel 对 View 无感知,生命周期由 LifecycleOwner 的 observe 机制自动管理。


架构图结合个人理解绘制,不同团队对边界的划分可能有偏差——结合具体业务理解即可。
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 模式下,通过 parentFragment 或 activity 作用域共享 ViewModel,是常见的路由状态共享方案。
六、MVI(Model–View–Intent)
6.1 核心命题
MVI 不是"稍微优化的 MVVM",而是对 Unidirectional Data Flow(UDF) 的严格实现:
- Intent:用户意图或系统事件(点击、下拉刷新、推送到达)
- Model:Reducer 处理 Intent,产出新 State
- State:不可变的 UI 状态快照,View 纯函数式渲染
- View:
State → 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 架构维度
| 模式 | 核心组成 | 职责划分 | 数据流动 |
|---|---|---|---|
| MVP | Model、View、Presenter | View 展示,Presenter 编排,Model 供数 | 双向(Presenter ↔ View 互相调用) |
| MVVM | Model、View、ViewModel | ViewModel 暴露可观察状态,View 只订阅 | 单向观察(状态驱动 UI) |
| MVI | Model、View、Intent/State | 交互即 Intent,State 不可变,View 纯渲染 | 严格 UDF(Intent → State → View) |
7.2 工程维度
| 特性 | MVP | MVVM | MVI |
|---|---|---|---|
| 学习曲线 | 低 | 中 | 中高 |
| 样板代码 | 接口较多(Contract) | 中等 | State/Intent 定义增加 |
| 状态管理 | 分散在 Presenter | ViewModel 多 LiveData 或 StateFlow | 中心化不可变 State |
| 可测试性 | 较好(Mock View) | 更好(无 View 依赖) | 最好(纯函数式 State 转换) |
| 生命周期 | 手动 attach/detach | ViewModel 自动管理 | 同 MVVM |
| 典型场景 | 遗留项目、Java 代码库 | Jetpack 官方推荐、XML + Fragment | Compose、复杂状态页面 |
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 + StateFlow | Jetpack 生态成熟,官方文档完善 |
| 新 Compose 项目 | MVVM 或 MVI | Compose 天然适配单一 State 驱动 |
| 状态复杂的单页(编辑器、表单 wizard) | MVI | 不可变 State 便于调试与回溯 |
8.2 按团队能力
- 小团队 / 快速交付:MVVM + 单一
UiState密封类(MVI 轻量版),不必引入完整 Reducer 框架 - 大团队 / 多端一致:严格 MVI + 状态日志,便于 Code Review 与问题复现
- 测试要求高:优先 MVI 或 MVVM + StateFlow,ViewModel 测试覆盖 State 转换
8.3 常见误区
- 把 MVVM 等同于 DataBinding 双向绑定——单向观察 + SSOT 才是 Google 推荐路径
- 把 MVI 等同于"用一个 sealed class 包 LiveData"——完整 MVI 还需区分 State 与 Event、保证 State 不可变
- 在 View 中直接调用 Repository——破坏分层,无论哪种模式都应禁止
- 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 原则对齐。