MVI (Model-View-Intent) 是一种新兴的架构模式,它是 MVVM 的进化版,核心思想源自 单向数据流 (Unidirectional Data Flow) 和 函数式编程。
在 2026 年的 Android 开发生态中,随着 Jetpack Compose 成为主流,MVI 已经成为构建复杂、高可靠性应用的首选架构模式。
核心理念:单向数据流
MVI 的核心在于状态(State)是唯一的真理来源,且数据流向永远是单向的闭环:
-
View --> Intent
-
Intent --> ViewModel
-
ViewModel --> State
-
State --> View
- end
-
1View (视图): 只负责渲染 State 和接收用户输入。用户输入不直接触发业务逻辑,而是转化为 Intent。
-
2Intent (意图): 描述用户想要做什么(例如:LoadUserIntent, ClickButtonIntent, SearchTextIntent)。它是 ViewModel 的唯一输入。
-
3ViewModel (模型/处理器):
- 接收 Intent。
- 处理业务逻辑(调用 Model/Repository)。
- 生成全新的、不可变的 (Immutable) State 对象。
- 发射 State 给 View。
-
4State (状态): 是一个不可变的数据类,包含界面渲染所需的所有数据(如:isLoading, data, errorMessage)。View 根据 State 重绘。
MVI 与 MVVM 的关键区别
虽然 MVI 和 MVVM 都有 ViewModel 和 Observer,但它们的数据处理方式截然不同:
| 特性 | MVVM (传统) | MVI (现代) |
|---|---|---|
| 状态管理 | 分散式。多个 LiveData/StateFlow 分别管理不同字段 (如 userName, isLoading, error)。 |
集中式。只有一个单一的 State 对象,包含所有 UI 状态。 |
| 数据可变性 | 通常是可变的 (Mutable)。直接修改某个字段的值 (state.userName = "New")。 |
不可变 (Immutable)。每次更新都创建一个全新的 State 对象 (state.copy(userName = "New"))。 |
| 输入方式 | 调用 ViewModel 的具体方法 (viewModel.loadUser(), viewModel.onButtonClick())。 |
发送 Intent 事件 (viewModel.acceptIntent(LoadUserIntent))。 |
| 状态一致性 | 弱。由于多个独立字段,可能出现"加载中但数据已显示"的中间不一致状态。 | 强。State 是原子性的,同一时刻界面只能反映一种确定的状态。 |
| 调试/回放 | 较难追踪状态变化历史。 | 极易。因为所有状态变更都是通过 Intent 触发的,可以轻松记录日志、回放甚至实现"时间旅行调试"。 |
| 适用场景 | 简单页面,逻辑不复杂。 | 复杂交互、多状态并发、需要严格状态控制的页面(尤其是 Compose)。 |
代码实战 (Kotlin + Jetpack Compose + StateFlow)
这是 2026 年标准的 MVI 实现方式。
第一步:定义状态 (State) - 不可变
使用 data class 和 sealed class 来确保不可变性。
// 定义所有可能的 UI 状态
data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val errorMessage: String? = null,
val searchText: String = "" // 所有界面需要的数据都在这里
)
// 定义用户意图 (Intent) - 也是不可变的
sealed class UserIntent {
object LoadUser : UserIntent()
data class UpdateSearch(val text: String) : UserIntent()
object Refresh : UserIntent()
}
第二步:定义 ViewModel - 状态机
使用 StateFlow 持有唯一状态,通过 reduce 模式更新状态。
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// 唯一的真实状态源
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow = _uiState.asStateFlow()
// 统一入口:处理所有 Intent
fun processIntent(intent: UserIntent) {
viewModelScope.launch {
when (intent) {
is UserIntent.LoadUser -> loadUser()
is UserIntent.UpdateSearch -> {
// 立即更新状态 (同步)
_uiState.update { it.copy(searchText = intent.text) }
// 可以触发搜索逻辑...
}
is UserIntent.Refresh -> loadUser()
}
}
}
private suspend fun loadUser() {
// 1. 设置加载状态 (生成新 State)
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
try {
// 2. 请求数据
val user = userRepository.getUser()
// 3. 设置成功状态 (生成新 State)
_uiState.update { it.copy(isLoading = false, user = user) }
} catch (e: Exception) {
// 4. 设置错误状态 (生成新 State)
_uiState.update { it.copy(isLoading = false, errorMessage = e.message) }
}
}
}
第三步:定义 View (Compose) - 渲染与反馈
View 只是状态的函数:UI = f(State)。
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
// 收集状态流
val state by viewModel.uiState.collectAsStateWithLifecycle()
// 渲染 UI (完全由 state 决定)
Box {
if (state.isLoading) {
CircularProgressIndicator()
} else if (state.errorMessage != null) {
Text(text = "错误: {state.errorMessage}")
Button(onClick = { viewModel.processIntent(UserIntent.Refresh) }) {
Text("重试")
}
} else {
Column {
Text(text = "用户名: {state.user?.name}")
TextField(
value = state.searchText,
onValueChange = {
// 用户输入转化为 Intent
viewModel.processIntent(UserIntent.UpdateSearch(it))
}
)
}
}
}
}
MVI 的优缺点分析
✅ 优点
- 1可预测性 (Predictability):由于状态是不可变的且集中管理,给定一个初始状态和一个 Intent,结果状态是确定的。这极大地减少了"由于时序问题导致的 Bug"。
- 2调试友好:你可以轻松打印每一个 Intent 和对应的 State 变化,甚至可以录制这些事件并在测试中回放。
- 3配置变更安全:屏幕旋转时,ViewModel 保留,State 保留,UI 自动恢复,无需手动保存 Bundle。
- 4完美契合 Compose:Jetpack Compose 的声明式 UI 本质就是 State -> UI,MVI 的单一状态流与 Compose 是天作之合。
- 5解耦:View 不知道业务逻辑,只负责发 Intent 和画 State;ViewModel 不知道 View 的具体实现。
❌ 缺点
- 1样板代码 (Boilerplate):需要定义大量的 State 类和 Intent 类(Sealed class),对于简单页面显得繁琐。
- 2内存开销:每次状态更新都创建新对象。虽然在现代 Kotlin/JVM 优化下影响很小,但在极高频率更新(如每秒 60 帧的动画坐标)场景下需注意。
- 3学习曲线:开发者需要理解"不可变性"、"归约 (Reduce)"、"单向数据流"等函数式编程概念。
- 4过度设计风险:对于极其简单的静态页面,强行上 MVI 可能杀鸡用牛刀。
常见实现库与模式变体
在实际开发中,我们通常不会手写所有的 update/copy 逻辑,而是借助库:
- 官方推荐: StateFlow + ViewModel (如上例)。
- Orbit-MVI: 一个流行的库,专门简化 MVI 的实现,提供了 container 概念来处理副作用和状态归约。
- Redux/Kotlin Redux: 直接移植前端 Redux 理念到 Android。
- Hilt/Dagger: 用于依赖注入,配合 MVI 使用。
总结:什么时候用 MVI?
-
强烈推荐:
- 使用 Jetpack Compose 的新项目。
- 页面逻辑复杂,状态多变(如:加载、成功、失败、空数据、分页、多条件筛选同时存在)。
- 对稳定性要求极高,需要严格测试状态流转的金融/交易类应用。
- 团队希望统一架构规范,减少"神奇 Bug"。
-
可以考虑不用:
- 极其简单的静态展示页(如"关于我们")。
- 纯动画或高性能图形渲染场景(此时状态更新频率过高,可能需要更底层的控制)。
一句话总结:
MVI 是通过不可变状态和单向数据流,将 UI 变成确定性函数的架构模式。它是 Jetpack Compose 时代的最佳拍档,解决了 MVVM 中状态分散和一致性问题,是现代 Android 架构的终极形态之一。