在 Android 开发里,大家最常听到的架构模式,基本绕不开两个:MVVM 和 MVI 。
很多人会有一个直觉印象:
MVVM 里的 UI 状态是"网状"的,
而 MVI 里的 UI 状态是"线性"的。
这句话有点抽象,但放到实际 Android 项目里,会变得非常具体。本文就从 Android 开发的视角,聊聊这两个模式在「UI 状态管理」上的差异,以及各自的优点和坑。
一、为什么要关心「UI 状态」?
无论你用的是 XML + ViewBinding,还是 Jetpack Compose,最后干的事情是一样的:
根据当前的业务状态,渲染出正确的 UI。
而"业务状态",就是我们说的 UI State,例如:
loading:是否正在加载items:列表数据error:错误信息empty:空页面状态selectedItemId:当前选中的 item
架构模式的差异,很大一部分体现在:这些状态是如何被组织、变化、传递和渲染的。
二、Android 上常见的 MVVM:UI 状态为什么容易变成"网状"
先看一个大家都很熟悉的 MVVM 写法(XML + ViewModel + LiveData 版本):

Activity / Fragment:

这里发生了什么?
- ViewModel 中有多条"状态线":
loadingitemserror
- View 层:
- 订阅多个 LiveData
- 再通过多个回调分别更新不同的 UI 控件
- View 和 ViewModel 的交互:
- View 可以调用多个方法:
load()、retry()、onRefresh()... - 这些方法内部各自操作不同的 LiveData
- View 可以调用多个方法:
为什么说它是"网状"的?
-
多个状态源彼此独立
loading、items、error各自维护,各自更新,没有统一视图,一眼看不出完整 UI 状态。 -
事件入口多
View 可以直接调各种方法,
load()、retry()、filter()......每个方法内部都可能改多个 LiveData。
-
调试难点
一旦出现"为什么这里 loading 没关掉""为什么 error 没消失"这种问题,你得沿着多个方法、多个 LiveData 去找来源,跟踪起来像在一张网里找线头。
因此,在很多 Android 项目中,MVVM 实现到后面,UI 状态的关系很容易演变成一张复杂的「状态网」。
这不是 MVVM 模式本身的理论问题,而是「常见实践方式」的问题。
MVVM 完全可以写得更"线性",但实际项目里,大部分人不会这样约束自己。
三、MVI 在 Android 上的实践:把状态拉成"一条线"
再看一段典型的 MVI 风格实现(以 StateFlow + sealed class 为例,适用于 Fragment 或 Jetpack Compose):
1. 定义单一 UI 状态

2. 定义用户意图(Intent / Event)

3. ViewModel:单一状态源 + 单入口事件

4. View 层(以 Compose 为例)

这里有什么不一样?
-
单一状态源
只有一个
state: StateFlow<UiState>暴露给 View。UI 所有需要的信息都在
UiState这个对象里。 -
单入口事件
View 不再直接调用
load()/retry()等业务方法,而是统一调用dispatch(intent)。这意味着:所有"意图"都走同一条通道进入 ViewModel。
-
状态更新模式统一
状态更新严格通过:

本质上就是:newState = reducer(oldState, intent)
这就是 MVI 常说的「Reducer」思想。
为什么说 MVI 更"线性"?
如果我们把每次状态更新看作一帧快照:
- 初始:
S0 = UiState(loading=false, items=[], error=null) - 用户发出
Load:S1 = loading=true - 接口成功:
S2 = loading=false, items=[...] - 用户发出
Retry:S3 = loading=true - 接口失败:
S4 = loading=false, error="xxx"
在时间维度上,状态就是一条清晰的线:
S0 → S1 → S2 → S3 → S4 → ...
每次变更都可以通过「哪个 Intent 触发」「哪个 Reducer 修改」追踪到,非常清晰。
数据流向也很单向:
View → Intent → ViewModel / Reducer → State → View
这就是 MVI 所强调的 Single Source of Truth(单一状态源) 和 Unidirectional Data Flow(单向数据流)。
四、对比总结:网状 vs 线性
可以用一个简单的对比表来帮助记忆:
| 对比项 | MVVM 常见实践 | MVI 实践 |
|---|---|---|
| 状态组织方式 | 多个 LiveData/StateFlow,分散管理 | 单一 UiState 对象统一管理 |
| 状态来源 | 多个字段,各自修改 | 单一 StateFlow,由 Reducer 统一生成 |
| 事件入口 | 多个公开方法:load()、retry()... |
通常一个 dispatch(intent) |
| 数据流向 | View ↔ ViewModel 双向调用 | View → Intent → State → View 单向 |
| 状态关系整体形态 | 容易形成状态网,依赖和时序分散 | 状态随时间线性演进,可回放、可追踪 |
| 调试 & 排错 | 需要到处找是谁改了哪个 LiveData | 只需看 Intent + Reducer 如何生成新 State |
| 是否可以写"干净" | 可以,但需要团队自律和强约束 | 机制本身就在逼你保持单向和统一状态 |
因此,从「状态形态」的视角来看:
- MVVM 在 Android 的主流写法往往会导致 UI 状态呈现为一张「网」;
- MVI 则强调将状态变化收敛为一条清晰的「线」。
五、是不是要"弃 MVVM 从 MVI"?
架构不应是"非黑即白"的选择,而是一场关于开发效率与系统稳定性的权衡。
1. MVVM 可以 MVI 化
即使你依然使用 MVVM,也完全可以引入 MVI 的核心思想:
- 使用一个
UiStatedata class 作为单一状态源; - 所有事件统一到
onEvent(intent)入口; - 状态更新统一用
copy()+ Reducer 的形式。
换句话说:架构名称不重要,实现风格更重要 。
很多团队嘴上说自己是 MVVM,实际上已经在写"轻量 MVI"了。
2. MVI 并不是免费的午餐
MVI 同样有成本:
- 需要定义 Intent / State / Effect 等一堆模型;
- 初期上手觉得"样板代码"较多;
- 对团队的抽象能力和规范意识要求更高。
适合的场景更多是:
- 业务流程复杂、状态多、易变;
- 需要较强的可测试性、可回放性、可追踪性;
- 团队对「单一状态源 + 单向数据流」理念有共识。
对于简单页面,用传统 MVVM + 一些良好实践可能更轻便。
六、如果你的项目正在用 MVVM,可以怎么往"线性状态"演进?
给几个渐进式的建议:
-
把零散 LiveData 收缩成一个 UiState
- 从:
val loading,val items,val error - 到:
val uiState: StateFlow<UiState>
- 从:
-
引入统一的事件入口
- 把多个
fun load(),fun retry(),fun onRefresh() - 合并为:
fun onEvent(event: UiEvent)
- 把多个
-
统一状态更新方式
- 禁止在 ViewModel 里到处
loading.value = false/error.value = ... - 统一写成:
updateState { it.copy(loading = false, error = ...) }
- 禁止在 ViewModel 里到处
-
在关键页面先试点
- 先在状态复杂的核心页面(比如首页、订单页)试 MVI 思路;
- 成功后再逐步推广。
这样做,你即使不改架构名,也已经获得了 MVI 带来的核心收益------线性的状态演进 + 可控的数据流。
七、结语
回到开头那句话:
MVVM 的 UI 状态是"网状"的,
MVI 的 UI 状态是"线性的"。
在 Android 实际项目里,大致可以理解为:
- 常见 MVVM 实践:多状态源 + 多事件入口 → 很容易演变成一张难以维护的状态网;
- MVI 实践:单一状态源 + 单向数据流 → 把状态更新串成一条明确的时间线。
对于我们日常写代码的人而言,更重要的是:
- 少一点"这个 loading 是谁关的?"这种灵魂拷问;
- 多一点"看一眼 State 就知道整个页面在干嘛"的放心。
如果你现在的项目已经是 MVVM,可以先从"一个 UiState + 一个 onEvent"开始,小步往前走,很快你就会感受到 UI 状态「从网到线」之后,调试和维护的舒适度差距。