有没有遇到过这样的场景?
你接手一个老项目,UI 状态乱得像小孩的玩具箱------
加载动画一直转、刷新不生效、点按钮没反应......
你以为是逻辑写错,结果是状态更新全乱了套。
问题的根源 其实不在你,而在架构:
没有清晰的"状态流向",UI 与数据逻辑搅在一起。
这时候,就轮到 MVI 架构(Model-View-Intent) 出场了。
🧠 一、MVI 到底是什么?为什么说它比 MVVM 更"纯粹"?
MVI 是 "Model - View - Intent" 的缩写,最早来源于前端(尤其是 React 和 Redux 思想),
后来被 Android 开发者借鉴,用于解决 复杂界面状态难以维护 的问题。
下面用表格清晰拆解 MVI 中 Model、View、Intent 三者的核心含义、职责及关键特征,帮你快速理清各模块定位。
| 核心组件 | 核心含义 | 核心职责 | 关键特征 / 注意事项 |
|---|---|---|---|
| Model(模型) | 特指 UI State(UI 状态) ,是描述当前界面 "样子" 的所有数据集合 | 1. 聚合界面所需全部状态(如加载中、数据列表、错误信息)2. 作为 View 渲染 UI 的唯一数据来源 | 1. 不可变 :状态一旦创建,只能通过生成新对象修改,避免并发问题2. 完整性 :包含界面所有可能状态,不遗漏任何展示场景(如空数据、加载失败)3. 数据驱动:View 完全依赖 State 渲染,无其他数据来源 |
| View(视图) | 对应 Activity/Fragment/Composable 等 UI 载体 | 1. 接收 State 并渲染 :根据 State 变化更新界面(如显示加载框、展示列表)2. 发送 Intent:将用户操作(点击、输入等)转换为 Intent 传给 ViewModel | 1. 无业务逻辑 :不处理数据请求、数据转换等,仅做 "渲染" 和 "转发"2. 生命周期感知 :需配合 repeatOnLifecycle 等 API 安全收集 State,避免内存泄漏3. 一次性事件处理:消费完导航、弹窗等一次性事件后,需通知 ViewModel 重置状态 |
| Intent(意图) | 封装用户的 所有交互行为,是 View 向业务层发送的 "指令" | 1. 统一用户操作入口(如 "加载数据""下拉刷新""点击 item")2. 向 ViewModel 传递操作所需参数(如点击 item 的 ID) | 1. 类型化 :通常用密封类(Sealed Class)定义,确保覆盖所有操作场景,避免遗漏2. 无业务逻辑 :仅描述 "做什么",不包含 "怎么做"(如 "加载数据" 不包含网络请求逻辑)3. 单向传递:只能从 View 流向 ViewModel,不反向传递 |
它的核心思想可以浓缩成 4 个字:
单向数据流(Unidirectional Data Flow)
什么意思?简单理解:
所有的数据变化,只能"从一个方向流动",不能反向倒流。
具体到 Android:
sql
View(界面) → Intent(用户意图) → ViewModel(业务逻辑)
→ State(新的界面状态) → View(重新渲染)
也就是说,UI 不再直接修改数据 ,
它只发送"意图",然后等状态回来。
这个过程就像点奶茶一样自然 👇
🧋 二、用"点奶茶"理解 MVI:单向数据流的故事
假设你走进奶茶店想点一杯"三分糖珍珠奶茶":
| 角色 | 对应 MVI 组件 | 职责说明 |
|---|---|---|
| 你 | View | 负责发送操作(点单)和展示结果(拿到奶茶) |
| 收银员 | ViewModel | 接收订单、处理业务、反馈状态 |
| 后厨 | Model(Repository) | 真正制作奶茶的数据层 |
| 奶茶状态 | State | 当前的制作进度(制作中 / 成功 / 失败) |
流程如下:
- 你(View)提交点单 → Intent(LoadUsers)
- 收银员(ViewModel)转交后厨 → Repository
- 后厨制作完毕 → 返回制作状态(State)
- 收银员通知你 → View 渲染新状态
整个信息流是单向的:
👉 View → ViewModel → Model → ViewModel → View
UI 不会直接跳进后厨改糖度。
这就是 MVI 最核心的理念------状态唯一、流向单一。
⚙️ 三、Jetpack MVI 三步落地实战
下面用一个"用户列表页"来展示 Jetpack + MVI 的组合拳。
🥤 第一步:定义 Intent(意图)与 State(状态)
kotlin
// 用户的操作意图
sealed class UserListIntent {
object LoadUsers : UserListIntent()
object RefreshUsers : UserListIntent()
data class ClickUser(val userId: String) : UserListIntent()
}
// 界面的状态
data class UserListState(
val isLoading: Boolean = false,
val users: List<User> = emptyList(),
val errorMsg: String? = null,
val navigateToDetail: String? = null
)
👉 Intent 是用户行为的抽象 ,
👉 State 是界面当前的唯一真相。
UI 不再存储数据副本,一切以 State 为准。
👩🍳 第二步:ViewModel------收银员的工作台
kotlin
class UserListViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UserListState())
val uiState: StateFlow<UserListState> = _uiState.asStateFlow()
fun handleIntent(intent: UserListIntent) {
when (intent) {
is UserListIntent.LoadUsers -> loadUsers()
is UserListIntent.RefreshUsers -> refreshUsers()
is UserListIntent.ClickUser -> navigate(intent.userId)
}
}
private fun loadUsers() {
if (_uiState.value.isLoading) return
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
try {
val users = userRepository.getUsers()
_uiState.update { it.copy(users = users, isLoading = false) }
} catch (e: Exception) {
_uiState.update { it.copy(errorMsg = "加载失败:${e.message}", isLoading = false) }
}
}
}
private fun navigate(id: String) {
_uiState.update { it.copy(navigateToDetail = id) }
}
}
这里的 StateFlow 就是"状态广播器"------
每当状态更新,UI 就会自动感知变化。
☕ 第三步:View------只负责点单和喝奶茶
kotlin
class UserListActivity : AppCompatActivity() {
private val viewModel: UserListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.handleIntent(UserListIntent.LoadUsers)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
}
private fun render(state: UserListState) {
binding.progressBar.isVisible = state.isLoading
(binding.userList.adapter as UserAdapter).submitList(state.users)
state.errorMsg?.let { showToast(it) }
state.navigateToDetail?.let { navigateToDetail(it) }
}
}
View 不再包含任何业务逻辑,只负责:
- 把操作转换为 Intent
- 监听 State 并更新界面
这让 UI 层变得清爽、可测、可控。
🧱 四、为什么 MVI 比 MVVM 更稳定?
| 对比点 | MVVM | MVI |
|---|---|---|
| 数据流方向 | 双向绑定(容易混乱) | 单向流动(清晰可追踪) |
| 状态管理 | 可能多个来源修改 | 状态集中存储于 State |
| 调试难度 | 中等(需跟踪多处变化) | 较低(状态链路可还原) |
| 可维护性 | 一般 | 高(每个状态可复现) |
简单来说:
MVVM 注重"数据绑定",
MVI 注重"状态统一与可预测性"。
MVI 把所有变化都聚焦到 ViewModel 的状态中,
只要还原这个 State,就能复现当时的 UI。
这对于调试和测试来说,是巨大的加分项。
💣 五、MVI 常见坑与避坑技巧
| 坑位 | 问题描述 | 解决方案 |
|---|---|---|
| ① State 可变 | 状态更改不触发 UI | 使用 data class + copy() 确保不可变 |
| ② 一次性事件重复执行 | Toast/跳转重复触发 | 消费后手动重置状态 |
| ③ 多重 Intent 并发 | 多个请求互相覆盖 | 加载前判断 isLoading 状态 |
| ④ ViewModel 状态泄漏 | 无生命周期感知 | 使用 repeatOnLifecycle 收集 State |
🧩 六、延伸:Jetpack 生态如何助力 MVI?
MVI 与 Jetpack 组件天然契合:
- ViewModel:保存状态,跨生命周期安全
- StateFlow:轻量级响应式状态流
- Coroutines:简化异步任务
- Hilt / Koin:依赖注入,模块化架构
- Paging 3:配合 MVI 状态流管理分页加载
你甚至可以把 MVI 拓展成更完整的架构:
View + ViewModel + StateFlow + Repository + UseCase + DI = 企业级架构雏形 🚀
🧠 七、MVI 中 Intent→ViewModel→State→View 的完整单向数据流
下面用 Mermaid 可视化流程图 展示 MVI 中 Intent→ViewModel→State→View 的完整单向数据流,每个环节标注核心动作和组件职责,帮你直观理解数据传递逻辑。
MVI 单向数据流可视化流程图

流程图核心逻辑解读
- 触发起点:用户交互用户的所有操作(如点击按钮、下拉刷新、输入文本)都是数据流的起点,这些行为会传递给 View 组件。
- 第一步:View 转换 Intent View 不处理业务逻辑,仅将用户行为封装成 Intent (通常用密封类定义,如
RefreshIntentClickItemIntent),确保操作指令标准化。 - 第二步:ViewModel 接收并处理 IntentViewModel 接收 Intent 后,根据指令调用 Repository(数据层)获取数据或执行业务逻辑(如网络请求、数据库读写)。
- 第三步:ViewModel 生成 State Repository 返回结果后,ViewModel 将 "数据 + 当前状态"(如加载中 / 成功 / 失败)整合为 不可变的 State 数据类,确保状态唯一且可追踪。
- 第四步:StateFlow 发射状态更新 ViewModel 通过
StateFlow(Jetpack 组件)将新 State 发射出去,StateFlow会自动通知所有订阅的 View。 - 终点:View 渲染 UIView 观察到 State 变化后,根据 State 中的数据(如列表数据、加载状态、错误信息)直接更新 UI,无需记忆历史状态,实现 "状态驱动 UI"。
✅ 一句话记住 MVI 架构的核心!
MVI = 单向数据流 + 不可变状态 + 状态驱动 UI。
把所有变化都封装进 State,让界面只需"渲染真相",不再纠结过程!