一、MVI 是什么
MVI = Model-View-Intent ,是一种单向数据流的架构模式。
它的核心思想可以用一句话概括:
View 只能"发 Intent",Model 只能"发 State",UI 只根据 State 渲染。
数据永远单向流动:
View → Intent → ViewModel → State → View
↑ ↓
└────────── 用户操作 ──────────┘
二、为什么从 MVVM 转向 MVI
Google 官方在 Compose 时代力推 MVI,原因是 MVVM 在 Compose 下暴露了三个问题:
| MVVM 的问题 | MVI 的解法 |
|---|---|
多个 LiveData / StateFlow 各自更新,UI 状态不一致 |
只用一个 UiState 快照,原子更新 |
| View 可以直接改 ViewModel 的属性,双向绑定混乱 | View 只能 sendIntent(),不能改状态 |
| 状态分散在多个可变量里,难以调试 | 状态是单一不可变数据类,可完整快照 |
Compose 的核心是"状态驱动 UI",MVI 恰好完美契合这个模型。
三、MVI 的三大组件(Compose 视角)
1. Model(State)------ 唯一状态源
用一个不可变数据类描述页面完整状态:
kotlin
data class RecommendUiState(
val isLoading: Boolean = false,
val data: List<RecommendItem> = emptyList(),
val error: String? = null,
val selectedIndex: Int = 0
)
关键点:
- 用
data class保证不可变 - 所有字段都有默认值,方便初始状态
- 一个页面只对应一个
UiState
2. View(Composable)------ 只读 + 只发 Intent
kotlin
@Composable
fun RecommendScreen(
viewModel: RecommendViewModel = koinViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 只读 State,根据 State 渲染
when {
uiState.isLoading -> Loading()
uiState.error != null -> ErrorView(uiState.error)
else -> ContentList(uiState.data)
}
// 用户操作 → 发 Intent
Button(onClick = { viewModel.sendIntent(RecommendIntent.Refresh) })
}
关键点:
- View 不能直接改
uiState - View 只能通过
sendIntent()告诉 ViewModel "我想干什么" - View 是 State 的纯函数:
UI = f(State)
3. Intent(用户意图)------ 所有操作的统一入口
kotlin
sealed class RecommendIntent {
object Refresh : RecommendIntent()
data class SelectItem(val index: Int) : RecommendIntent()
data class PlayMusic(val position: Int, val item: RecommendData) : RecommendIntent()
}
关键点:
- 用
sealed class穷举所有可能的操作 - 命名用动词,描述"用户想做什么"
- 与
UiEffect区分:Intent 是输入 ,Effect 是输出
四、ViewModel 的职责
ViewModel 是 MVI 的中枢,做三件事:
1. 持有唯一状态
kotlin
private val _uiState = MutableStateFlow(RecommendUiState())
val uiState: StateFlow<RecommendUiState> = _uiState.asStateFlow()
注意 :对外只暴露 StateFlow(只读),内部用 MutableStateFlow。
2. 接收 Intent,更新 State
kotlin
fun sendIntent(intent: RecommendIntent) {
viewModelScope.launch {
intentFlow.emit(intent)
}
}
private fun handleIntent(intent: RecommendIntent) {
when (intent) {
is RecommendIntent.Refresh -> refresh()
is RecommendIntent.SelectItem -> selectItem(intent.index)
is RecommendIntent.PlayMusic -> playMusic(intent.position, intent.item)
}
}
3. 通过 Effect 通知一次性事件
kotlin
private val _effect = Channel<RecommendUiEffect>()
val effect = _effect.receiveAsFlow()
// 弹 Toast、导航、播放音乐等一次性事件
_effect.send(RecommendUiEffect.PlayList(list, position, tag))
State 和 Effect 的区别:
| 维度 | UiState | UiEffect |
|---|---|---|
| 性质 | 状态(持久) | 事件(瞬时) |
| 示例 | 列表数据、加载中 | 弹 Toast、导航、播放 |
| 旋转屏幕 | 自动恢复 | 不会重放 |
| 消费次数 | 可多次读取 | 消费一次即消失 |
五、完整示例(Compose + Kotlin)
UiState
kotlin
data class RecommendUiState(
val isLoading: Boolean = false,
val data: List<RecommendItem> = emptyList(),
val error: String? = null
)
Intent
kotlin
sealed class RecommendIntent {
object Load : RecommendIntent()
data class Click(val id: String) : RecommendIntent()
}
Effect
kotlin
sealed class RecommendUiEffect {
data class ShowToast(val msg: String) : RecommendUiEffect()
data class Navigate(val route: String) : RecommendUiEffect()
}
ViewModel
kotlin
class RecommendViewModel : ViewModel() {
private val _uiState = MutableStateFlow(RecommendUiState())
val uiState: StateFlow<RecommendUiState> = _uiState.asStateFlow()
private val _effect = Channel<RecommendUiEffect>(Channel.BUFFERED)
val effect = _effect.receiveAsFlow()
private val intentChannel = Channel<RecommendIntent>(Channel.UNLIMITED)
init {
viewModelScope.launch {
intentChannel.consumeAsFlow().collect { handleIntent(it) }
}
}
fun sendIntent(intent: RecommendIntent) {
viewModelScope.launch { intentChannel.send(intent) }
}
private suspend fun handleIntent(intent: RecommendIntent) {
when (intent) {
is RecommendIntent.Load -> load()
is RecommendIntent.Click -> click(intent.id)
}
}
private suspend fun load() {
_uiState.update { it.copy(isLoading = true) }
try {
val data = repository.fetch()
_uiState.update { it.copy(isLoading = false, data = data) }
} catch (e: Exception) {
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
}
private suspend fun click(id: String) {
_effect.send(RecommendUiEffect.ShowToast("点击了 $id"))
}
}
Compose View
kotlin
@Composable
fun RecommendScreen(viewModel: RecommendViewModel = koinViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// 消费 Effect
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is RecommendUiEffect.ShowToast -> toast(effect.msg)
is RecommendUiEffect.Navigate -> navigate(effect.route)
}
}
}
// 根据 State 渲染
when {
uiState.isLoading -> Loading()
uiState.error != null -> ErrorView(uiState.error)
else -> LazyColumn {
items(uiState.data) { item ->
ItemView(item, onClick = {
viewModel.sendIntent(RecommendIntent.Click(item.id))
})
}
}
}
}
六、MVI 的核心原则(背下来)
| 原则 | 说明 |
|---|---|
| 单一状态源 | 一个页面只有一个 UiState |
| 状态不可变 | UiState 用 data class,只能通过 copy() 更新 |
| 单向数据流 | View → Intent → ViewModel → State → View |
| View 无状态 | View 不持有业务状态,只渲染 State |
| State 与 Effect 分离 | 持久状态用 State,一次性事件用 Effect |
| Intent 穷举 | 用 sealed class 穷举所有用户操作 |
七、State vs Effect:最容易搞混的点
这是 MVI 面试必问,一定要分清:
| 场景 | 放哪里 | 原因 |
|---|---|---|
| 列表数据 | State | 需要持久显示 |
| 加载中 | State | UI 需要持续显示 |
| 错误信息 | State | UI 需要持续显示 |
| 弹 Toast | Effect | 弹一次就消失 |
| 导航跳转 | Effect | 触发一次就够 |
| 播放音乐 | Effect | 触发一次就够 |
| 显示 Dialog | Effect | 触发一次就够 |
判断标准 :旋转屏幕后,这个东西还应该存在吗?
- 应该存在 → State
- 不应该存在 → Effect
八、MVI 在 Compose 下的完整数据流
┌─────────────────────────────────────────────┐
│ Composable │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ collectAsState│ │ sendIntent() │ │
│ └──────┬───────┘ └────────┬────────┘ │
└─────────┼──────────────────────┼────────────┘
│ State │ Intent
▼ ▼
┌─────────────────────────────────────────────┐
│ ViewModel │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ _uiState │ │ intentChannel │ │
│ │ (StateFlow) │ │ (Channel) │ │
│ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │
│ │ handleIntent() │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Repository │ │
│ └─────────────┘ │
└─────────────────────────────────────────────┘
九、总结
| 维度 | 说明 |
|---|---|
| 核心思想 | 单向数据流,State 驱动 UI |
| 三大件 | UiState + Intent + Effect |
| ViewModel 职责 | 持有 State、处理 Intent、发送 Effect |
| View 职责 | 读 State 渲染、发 Intent 通知 |
| 与 Compose 的关系 | Compose 的"状态驱动 UI"天然契合 MVI |
| 与 MVVM 的区别 | MVI 用单一 State 替代多 LiveData,单向数据流替代双向绑定 |
| 最易混淆点 | State(持久)vs Effect(瞬时) |
一句话记住 MVI:
UI = f(State),用户操作 = Intent,一次性事件 = Effect。