聊聊 MVI 的发展史和现代 Android 实践
1. 引言:抛弃刻板印象
MVI 是 Android 开发中绕不开的架构模式。它提供清晰、可预测的数据流,是构建稳定 UI 的得力工具。 但现实中,“MVI” 常被误解为单一、僵化的模式。有人将其与 “Redux 风格实现” 划等号,导致很多团队因对 Redux 的刻板印象而回避 MVI。其实 MVI 针对 Android 场景进行了灵活适配,能够在更加易用的同时保留其核心思想。
本文将探索 MVI 的根源、它要解决的问题,以及拥抱其 “适应性” 而非 “僵化规则” 的重要性。架构的意义在于解决问题,而非盲目遵循教条。
2. 架构先驱:孕育 MVI 的模式
MVI 不是凭空出现的,它是架构演进的产物。在它之前,已有诸多模式为其铺垫,开发者们不断反思现有方案,推动着架构的迭代(参考 [1][2][3])。
很多人认为 MVI 源自 Redux(2015),但追根溯源,它的真正先驱是:Model-View-Controller(MVC)和后来的响应式编程模式。
1979年: Model-View-Controller(MVC)
MVC 是最古老、最具影响力的架构模式之一,诞生于 20 世纪 70 年代。它将应用拆分为三大组件:
Model:管理数据和业务逻辑 View:处理用户输入(如点击) Controller:处理用户输入,驱动 Model 更新
2014年5月:Flux
2014 年,Facebook 推出了 Flux,聚焦数据流向管理。它的核心是:
Actions:描述发生的事件 Dispatcher:分发 Actions 到 Store Store:持有应用状态,响应 Actions 更新状态 Views:监听 Store 变化,触发重新渲染
Flux 最关键的创新是 “单向、循环数据流”,解决了共享可变状态的问题。这种 “可预测、线性” 的数据流,成为 MVI 的核心思想。
2014年12月:MVI(响应式 MVC)
André Staltz 从响应式编程视角重新审视 MVC。在其博客 《Reactive MVC and the Virtual DOM》[4] 中,提出 “响应式 MVC” 概念。
MVI 借鉴了 Flux 的单向数据流,简化了实现:去掉 Dispatcher,让 Intent 直接驱动 Model,用可观测流连接 Model、用户意图、状态和 UI 渲染,形成 “纯响应式转换”:
Intent 替代 Controller:将用户交互转化为事件流 Model 响应事件流,更新应用状态 View 监听状态流,触发渲染
这种重构后的架构被称为 Model-View-Intent(MVI),这是 MVI 模式的首次正式提出。它继承了 MVC 的 “单向数据流”,并天然适配响应式编程的 “可观测流”。
3. MVI 的起源:模式如何成型
MVI 模式由 André Staltz 在 2014 年(Redux 诞生前)首次提出,最初用于 Cycle.js 框架,而非 Android。直到 2015 年 Redux 流行后,MVI 才在 Android 生态中被广泛讨论。
MVI 模式的实践定义
Model-View-Intent(MVI)是一种响应式架构模式,强制实现 “单一、不可变的状态” 和 “单向事件流”,核心组件:
Model:持有应用状态 View:渲染状态 Intent:替代 Controller,作为用户交互的事件流 (此处省略 MVI 流程图描述,核心逻辑:用户交互生成 Intent,Intent 驱动 Model 生成新状态,View 监听状态更新并渲染。)
我们可以从 André Staltz 2015 年的博客《Unidirectional User Interface Architectures 》[5]中,阅读 MVI 的定义和组件描述。
他的设计聚焦 “最小化用户意图 → 状态变化 → UI 渲染” 的循环,形式化了数据流,让状态变化可预测。这种思想借鉴了 MVC、Flux、Redux(后出现),但 MVI 是为响应式系统量身定制的独特架构。
代码示例
2015 年,Jannis Dornemann 在博客中,用数学表达式诠释了 MVI 的核心思想:
对应的代码实现逻辑大致如下(伪代码):
// Intent 是用户交互事件流
val intents: Flow<Intent> = view.userInteractions()
// Model 处理 Intent,生成状态流
val states: Flow<State> = intents.map { intent ->
model.reduce(intent)
}
// View 监听状态流,渲染 UI
states.collect { state ->
view.render(state)
}
你可以在代码中看到这个数学表达式的直接体现。
不过,这个 “理想模型” 如何真正落地到 Android 开发中?别着急,我们继续拆解。
4. 误区:MVI 不是什么
随着 MVI 在 Android 生态流行,一个常见误区出现了:“MVI 必须是 Redux 风格的单向状态管理,必须有全局 Store、Reducer”。甚至有人质疑:“如果 Android 实现没有这些,能叫 MVI 吗?”
误区 1:MVI = Redux
最常见的误解是 “MVI 就是 Redux”。但事实是:
MVI 由 André Staltz 在 2014 年在博客文章 《Reactive MVC》[6] 提出,远早于 2015 年 Dan Abramov 发布的 Redux[7]。 MVI 的核心思想(单向数据流、不可变状态)确实启发了 Redux,但两者有本质区别。
Redux 是 “集中式全局状态管理”,而 MVI 是 “分布式状态管理(每个模块 / 页面独立)”。两者的相似点(如状态不可变、基于事件流)下,隐藏着架构差异:
简言之:Android 中的 MVI 实现,不一定要严格遵循 Redux 模式。
误区 2:MVI = MVVM(或其他模式)
另一个误区是 “MVI 只是换皮的 MVVM”。 MVVM 架构曾经一度被 Android 官方强推,下面是官方经典 MVVM 架构图:
很多开发者在实践中,不自觉地用 MVI 的方式写 MVVM(或反之),因为两者确实有相似基础:
都用可观测流传递状态 都用不可变状态更新 UI 都强调 “响应式编程”(尤其是 Android 中的 Jetpack 组件)
相似性背后,核心差异在于数据流和架构约束:
这些差异的存在源于 MVI 和 MVVM 的本质的区别:View 是否持有状态。MVVM 的 View 持有状态,所以需要与 ViewMoel 双向通信,而 MVI 追求所有状态管理发生在 ViewModel。
当你 MVVM 代码中已经没有 DataBinding、没有需要向 ViewModel 传递参数(状态)的代码时,实际上就是一段准 MVI 代码了。
下面是一段用 MVVM 实现的 MVI 代码示例,也是当今项目中非常流行的一种写法:
// ViewModel exposing multiple state properties
classUserViewModel : ViewModel() {
// Backing properties
privateval _username = MutableStateFlow("")
val username: StateFlow<String> = _username.asStateFlow()
privateval _age = MutableStateFlow(0)
val age: StateFlow<Int> = _age.asStateFlow()
// Functions to update state
funonUsernameChanged(newName: String) {
_username.value = newName
}
funonAgeIncrement() {
_age.value += 1
}
funonAgeDecrement() {
if (_age.value > 0) _age.value -= 1
}
}
@Composable
funUserScreen(viewModel: UserViewModel = viewModel()) {
// Collect state from the ViewModel
val username by viewModel.username.collectAsStateWithLifecycle()
val age by viewModel.age.collectAsStateWithLifecycle()
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = username,
onValueChange = { viewModel.onUsernameChanged(it) },
label = { Text("Username") }
)
Text(text = "Age: $age")
Button(onClick = { viewModel.onAgeIncrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Increase Age")
}
Button(onClick = { viewModel.onAgeDecrement() }, modifier = Modifier.padding(top = 8.dp)) {
Text("Decrease Age")
}
}
}
这样的代码很实用,没有大问题,但是在约束性上稍显不足,仍然有提升空间。真正的 MVI 应严格遵循 “单向数据流、不可变状态、Intent 驱动” 的约束。
5. 真正的 MVI 应该怎么写
通过分析一些常见误解,结合相关模式,我们可总结出 真正 MVI 的核心特征:
单一不可变状态模型:状态是集中、不可变的,通常封装在 ViewState 中。 状态托管在 Model 中:ViewState 由 Model 管理,而非分散在 View 层。 Intent 作为用户交互的统一抽象:所有用户操作(点击、输入等)都封装为 Intent,作为事件流传递。 Intent 触发状态转换:Intent 传入 Model 后,通过 Reducer 生成新状态。 单向数据流:严格遵循 Intent → State → View 的流向,禁止反向依赖。
MVI 所需的数据模型
建议 MVI框架中都有以下几个数据模型,加强框架的可约束性
UiState:定义 UI 关注的 State,注意需要是 Immutable 的。 Intent:定义来自 UI 的 Action SideEffect:剥离那些纯业务逻辑之外的数据状态为副作用,比如日志、埋点等
// UI State data class (immutable)
dataclassUserUiState(
val username: String = "",
val age: Int = 0,
val isLoading: Boolean = false
)
// User intents: user actions
sealedinterfaceUserIntent{
dataclassChangeUsername(val newName: String) : UserIntent
object IncrementAge : UserIntent
object DecrementAge : UserIntent
}
// Side effects (one-time events)
sealedinterfaceUserSideEffect{
dataclassShowMessage(val message: String) : UserSideEffect
}
MVI 中 ViewModel 的职责
在 Android 中,ViewModel 通常承担以下职责:
暴露可监听的 State(如 StateFlow<ViewState>),主要务必使 Immutable 的提供公共函数接收和处理 Intent,通过应用 reducer 逻辑更新 State。:如网络请求、数据库操作,结果转化为 Intent 或直接更新 State 可以在 StateFlow 之外同一个,处理副作用的 Flow,确保 reducer 逻辑是纯函数
classUserViewModel : ViewModel() {
privateval _sideEffects = MutableSharedFlow<UserSideEffect>()
val sideEffects = _sideEffects.asSharedFlow()
privateval _uiState = MutableStateFlow(UserUiState())
val uiState = _uiState.asStateFlow()
// Process a single intent immediately
funprocessIntent(intent: UserIntent) {
when (intent) {
is UserIntent.ChangeUsername -> {
_uiState.update { it.copy(username = intent.newName) }
}
UserIntent.IncrementAge -> {
_uiState.update { it.copy(age = it.age + 1) }
viewModelScope.launch {
_sideEffects.emit(UserSideEffect.ShowMessage("Age increased!"))
}
}
UserIntent.DecrementAge -> {
if (current.age > 0) {
_uiState.update { current ->
current.copy(age = current.age - 1)
}
}
}
}
}
}
为什么 MVI 难以 “标准化”?
MVI 没有官方标准实现,因为它本质是 “架构思想”,而非 “固定模板”。不同团队可根据需求调整(如是否用全局 Store、如何处理副作用),但核心原则不能丢。
这也导致一个矛盾:开发者可能因 “没有统一标准” 而困惑,但正是这种灵活性,让 MVI 适配不同场景。
6. 最终思考: 如何选型?
“是否值得用” 取决于团队和场景:如果你是 solo 开发者,MVI 的约束可能显得冗余;但团队协作时,它的 “可预测性” 能大幅降低沟通成本。
不要为了 “用 MVI” 而用 MVI:先理解问题,再选工具。如果项目简单,MVVM 或 MVC 足够;复杂场景下,MVI 的优势才会凸显。
MVI 考验的不只是技术,更是 “软件工程思维”:它强制你思考 “状态如何变化”“事件如何流转”,倒逼开发者写出更整洁、可维护的代码。
MVI 不一定要用 Redux 式全局 Store:小模块中,MVI 可轻量化实现(如单个 ViewModel + 局部 State),关键是遵循核心原则。
别纠结 “是不是真正的 MVI”:模式的价值在于解决问题。只要你的架构符合 “单向数据流、不可变状态、Intent 驱动” 的核心思想,就是 “MVI 精神” 的实践。
André Staltz 博客:Architectural patterns for interactive programs: https://staltz.com/some-problems-with-react-redux
[2]Nothing new in React and Flux except one thing: https://staltz.com/nothing-new-in-react-and-flux-except-one-thing
[3]Unidirectional User Interface Architectures: https://staltz.com/unidirectional-user-interface-architectures#:~:text=Model-Vie
[4]Reactive MVC and the Virtual DOM: https://www.futurice.com/blog/reactive-mvc-and-the-virtual-dom
[5]Unidirectional User Interface Architectures: https://staltz.com/unidirectional-user-interface-architectures#:~:text=Model-View-Intent
[6]Reactive MVC: https://www.futurice.com/blog/reactive-mvc-and-the-virtual-dom
[7]Redux: https://redux.js.org/understanding/history-and-design/history-of-redux#2015-the-birth-of-redux