从 MVVM 到 MVI:为什么说 MVVM 的 UI 状态像“网”,而 MVI 像“一条线”?

0 阅读6分钟

在 Android 开发里,大家最常听到的架构模式,基本绕不开两个:MVVMMVI
很多人会有一个直觉印象:

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 版本):

image.png

Activity / Fragment:

image.png

这里发生了什么?

  • ViewModel 中有多条“状态线”:
    • loading
    • items
    • error
  • View 层:
    • 订阅多个 LiveData
    • 再通过多个回调分别更新不同的 UI 控件
  • View 和 ViewModel 的交互:
    • View 可以调用多个方法:load()retry()onRefresh()
    • 这些方法内部各自操作不同的 LiveData

为什么说它是“网状”的?

  1. 多个状态源彼此独立
    loadingitemserror 各自维护,各自更新,没有统一视图,一眼看不出完整 UI 状态。

  2. 事件入口多
    View 可以直接调各种方法,load()retry()filter()……
    每个方法内部都可能改多个 LiveData。

  3. 调试难点
    一旦出现“为什么这里 loading 没关掉”“为什么 error 没消失”这种问题,你得沿着多个方法、多个 LiveData 去找来源,跟踪起来像在一张网里找线头。

因此,在很多 Android 项目中,MVVM 实现到后面,UI 状态的关系很容易演变成一张复杂的「状态网」。

这不是 MVVM 模式本身的理论问题,而是「常见实践方式」的问题。
MVVM 完全可以写得更“线性”,但实际项目里,大部分人不会这样约束自己。


三、MVI 在 Android 上的实践:把状态拉成“一条线”

再看一段典型的 MVI 风格实现(以 StateFlow + sealed class 为例,适用于 Fragment 或 Jetpack Compose):

1. 定义单一 UI 状态

image.png

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

image.png

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

image.png

4. View 层(以 Compose 为例)

image.png

这里有什么不一样?

  1. 单一状态源
    只有一个 state: StateFlow<UiState> 暴露给 View。
    UI 所有需要的信息都在 UiState 这个对象里。

  2. 单入口事件
    View 不再直接调用 load() / retry() 等业务方法,而是统一调用 dispatch(intent)
    这意味着:所有“意图”都走同一条通道进入 ViewModel

  3. 状态更新模式统一
    状态更新严格通过:

image.png

本质上就是:newState = reducer(oldState, intent)
这就是 MVI 常说的「Reducer」思想。

为什么说 MVI 更“线性”?

如果我们把每次状态更新看作一帧快照:

  • 初始:S0 = UiState(loading=false, items=[], error=null)
  • 用户发出 LoadS1 = loading=true
  • 接口成功:S2 = loading=false, items=[...]
  • 用户发出 RetryS3 = 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 的核心思想:

  • 使用一个 UiState data class 作为单一状态源;
  • 所有事件统一到 onEvent(intent) 入口;
  • 状态更新统一用 copy() + Reducer 的形式。

换句话说:架构名称不重要,实现风格更重要
很多团队嘴上说自己是 MVVM,实际上已经在写“轻量 MVI”了。

2. MVI 并不是免费的午餐

MVI 同样有成本:

  • 需要定义 Intent / State / Effect 等一堆模型;
  • 初期上手觉得“样板代码”较多;
  • 对团队的抽象能力和规范意识要求更高。

适合的场景更多是:

  • 业务流程复杂、状态多、易变;
  • 需要较强的可测试性、可回放性、可追踪性;
  • 团队对「单一状态源 + 单向数据流」理念有共识。

对于简单页面,用传统 MVVM + 一些良好实践可能更轻便。


六、如果你的项目正在用 MVVM,可以怎么往“线性状态”演进?

给几个渐进式的建议:

  1. 把零散 LiveData 收缩成一个 UiState

    • 从:val loading, val items, val error
    • 到:val uiState: StateFlow<UiState>
  2. 引入统一的事件入口

    • 把多个 fun load(), fun retry(), fun onRefresh()
    • 合并为:fun onEvent(event: UiEvent)
  3. 统一状态更新方式

    • 禁止在 ViewModel 里到处 loading.value = false / error.value = ...
    • 统一写成:updateState { it.copy(loading = false, error = ...) }
  4. 在关键页面先试点

    • 先在状态复杂的核心页面(比如首页、订单页)试 MVI 思路;
    • 成功后再逐步推广。

这样做,你即使不改架构名,也已经获得了 MVI 带来的核心收益——线性的状态演进 + 可控的数据流


七、结语

回到开头那句话:

MVVM 的 UI 状态是“网状”的,
MVI 的 UI 状态是“线性的”。

在 Android 实际项目里,大致可以理解为:

  • 常见 MVVM 实践:多状态源 + 多事件入口 → 很容易演变成一张难以维护的状态网;
  • MVI 实践:单一状态源 + 单向数据流 → 把状态更新串成一条明确的时间线。

对于我们日常写代码的人而言,更重要的是:

  • 少一点“这个 loading 是谁关的?”这种灵魂拷问;
  • 多一点“看一眼 State 就知道整个页面在干嘛”的放心。

如果你现在的项目已经是 MVVM,可以先从“一个 UiState + 一个 onEvent”开始,小步往前走,很快你就会感受到 UI 状态「从网到线」之后,调试和维护的舒适度差距。