MVI 和 MVVM 别再选错了:2026 年 Android 架构选型的真实答案
MVI 和 MVVM 别再选错了:2026 年 Android 架构选型的真实答案
在 Android 架构里,MVI 和 MVVM 经常被讨论成“谁更先进”的问题。但在真实项目里,它们不是信仰对决,而是成本和收益的取舍。
先给结论:
简单页面用 MVVM,复杂状态页面用 MVI;现代 Android 项目里,最常见、也最稳妥的方案,是“状态驱动型 MVVM 起步,复杂页面局部升级到 MVI”。
换句话说,不要为了显得架构高级而全项目硬套 MVI,也不要因为 MVVM 写起来舒服,就在复杂页面里继续堆散装函数和散装状态。
这篇文章只解决一个问题:2026 年做 Android 页面时,到底什么时候该用 MVVM,什么时候该用 MVI?
一、先看选型答案
如果你不想看长篇分析,可以直接按下面这张表选:
| 场景 | 更适合 |
|---|---|
| 静态展示页、设置页、详情页 | MVVM |
| 只有加载、成功、失败、重试 | 状态驱动型 MVVM |
| 多字段表单、筛选器、分页、局部刷新、撤销、批量操作 | MVI |
| 状态之间有严格互斥关系 | MVI |
| 需要记录用户操作、复现线上 Bug、做回放测试 | MVI |
| KMP 多端共享业务状态机 | MVI |
| 团队新人多、页面容易被不断加需求 | 倾向 MVI |
一句话:
MVVM 追求开发效率,MVI 追求状态确定性。
MVVM 的优势是直接、轻量、代码少。MVI 的优势是约束强、可回放、状态边界清楚。问题从来不是哪个架构更好,而是你的页面是否值得为“确定性”付出更多模板代码。
二、MVVM 和 MVI 真正差在哪里
很多文章会说 MVI 是单向数据流,MVVM 是双向绑定,或者说 MVI 更响应式。这些说法都不够抓重点。
在现代 Android 里,尤其是 Compose + StateFlow 的项目中,MVVM 和 MVI 的差异主要体现在两层:
- 行为入口不同:UI 如何通知 ViewModel 做事。
- 契约完整度不同:MVVM 可以只收敛
UiState,MVI 会进一步把Intent和Effect也纳入契约。
1. MVVM:函数驱动
MVVM 的典型写法是:ViewModel 暴露状态,UI 直接调用 ViewModel 的函数。
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState = _uiState.asStateFlow()
fun loadOrders() {
// load data
}
fun retry() {
loadOrders()
}
fun deleteOrder(orderId: String) {
// delete order
}
}
UI 层直接调用这些方法:
ErrorScreen(
onRetry = { viewModel.retry() }
)
OrderList(
onDelete = { orderId -> viewModel.deleteOrder(orderId) }
)
这种模式的好处非常明显:直观、少代码、开发快。
但它的问题也很明显:随着页面变复杂,ViewModel 会暴露越来越多 public 方法。最后 UI 层到处都能调用业务函数,调用顺序、调用时机、调用入口越来越难控制。
2. MVI:Intent 驱动
标准 MVI 不让 UI 直接调用业务函数。UI 只能发送一个描述用户行为的数据对象,也就是 Intent。
sealed interface OrderIntent {
data object LoadOrders : OrderIntent
data object Retry : OrderIntent
data class DeleteOrder(val orderId: String) : OrderIntent
}
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState = _uiState.asStateFlow()
fun onIntent(intent: OrderIntent) {
when (intent) {
OrderIntent.LoadOrders -> loadOrders()
OrderIntent.Retry -> loadOrders()
is OrderIntent.DeleteOrder -> deleteOrder(intent.orderId)
}
}
private fun loadOrders() {
// load data
}
private fun deleteOrder(orderId: String) {
// delete order
}
}
UI 层不再知道 ViewModel 内部有哪些业务函数:
ErrorScreen(
onRetry = { viewModel.onIntent(OrderIntent.Retry) }
)
OrderList(
onDelete = { orderId -> viewModel.onIntent(OrderIntent.DeleteOrder(orderId)) }
)
MVI 的核心不是多写一个 onIntent,而是把所有用户行为都变成了可枚举、可记录、可测试的数据。
三、MVI 在做什么
说清楚 MVI 之前,先不要急着谈收益。MVI 本质上是在给页面建立一套输入输出契约:UI 把用户行为交给 ViewModel,ViewModel 处理后再把新的页面状态交还给 UI。
这条数据流通常会被叫做 UDF(Unidirectional Data Flow,单向数据流):
View 发送 Intent -> ViewModel 处理 -> 输出 UiState / Effect -> View 渲染
但要注意,UDF 不是 MVI 独占的东西,现代 MVVM 也完全可以是 UDF。 比如 Compose 里很常见的状态驱动型 MVVM,就是 ViewModel 暴露 UiState,UI 根据 UiState 渲染,再把点击事件交回 ViewModel。
UDF 可以看 从UseCase的理念看Android当前的应用架构设计指南 这篇代码
MVI 和状态驱动型 MVVM 的差别不在于“谁能不能 UDF”,而在于 MVI 会把这条单向数据流里的输入端也强制数据化。
所以 MVI 的契约通常由三部分组成:
- UiState:页面当前长什么样。
- Intent:用户或系统发生了什么。
- Effect:只消费一次的副作用,比如 Toast、导航、弹窗。
1. UiState 合并页面状态
先看 UiState。它负责把页面状态收敛成一个稳定快照。
sealed interface OrderUiState {
data object Loading : OrderUiState
data class Success(val orders: List<Order>) : OrderUiState
data class Error(val message: String) : OrderUiState
}
过去很多页面会把状态拆成 isLoading、hasError、data、errorMessage 这种零散字段。问题是这些字段可以随便组合,最后很容易出现“既在 Loading,又在 Error”的非法状态。
sealed interface UiState 的好处,是让页面在同一时刻只能处于一种明确状态:Loading、Success 或 Error。
但这里要点名一句:UiState 不是 MVI 独占的东西,现代 MVVM 也经常这么写。 如果你的页面只是想解决 Loading、Success、Error 的互斥问题,状态驱动型 MVVM 就够了。
MVI 真正继续往前走的地方,是把行为也纳入契约。
2. 把行为入口数据化
MVVM 最大的舒适点,是 UI 可以直接调用 ViewModel 函数:
fun loadOrders()
fun retry()
fun deleteOrder(id: String)
页面简单时,这种写法很舒服。但页面复杂后,ViewModel 很容易暴露越来越多 public 函数,UI 层任何地方都可以调用业务逻辑,调用顺序也很难管。
MVI 会先把入口收成一个:
fun onIntent(intent: OrderIntent)
所有可触发行为都必须先进入 OrderIntent:
sealed interface OrderIntent {
data object Refresh : OrderIntent
data object LoadNextPage : OrderIntent
data class SelectOrder(val id: String) : OrderIntent
data class DeleteOrder(val id: String) : OrderIntent
data object UndoDelete : OrderIntent
data class ApplyFilter(val filter: OrderFilter) : OrderIntent
data object ClearFilter : OrderIntent
data object Submit : OrderIntent
}
到这里为止,MVI 完成了第一件关键动作:把行为入口数据化。
View 不再直接说“调用 deleteOrder()”,而是说“发生了 DeleteOrder 这个行为”。
3. 把状态流转显式化
只有 Intent 还不够。复杂页面真正麻烦的地方,是同一个行为在不同状态下不一定都合法。
比如提交页:
sealed interface SubmitState {
data object Editing : SubmitState
data object Validating : SubmitState
data object Submitting : SubmitState
data class Failed(val message: String) : SubmitState
data class Submitted(val orderId: String) : SubmitState
}
sealed SubmitState 可以保证“正在提交”和“已提交成功”不会同时出现。但它还没有回答另一个问题:什么时候允许提交?什么时候应该拒绝提交?
规则其实是这样的:
| 当前状态 | 收到行为 | 结果 |
|---|---|---|
Editing |
Submit |
开始校验 |
Validating |
Submit |
忽略 |
Submitting |
Submit |
忽略 |
Submitted |
Submit |
忽略 |
Failed |
EditAgain |
回到编辑 |
MVI 会把这类规则集中写在 onIntent 后面:
sealed interface SubmitIntent {
data object Submit : SubmitIntent
data object EditAgain : SubmitIntent
}
fun onIntent(intent: SubmitIntent) {
when (val state = _state.value) {
SubmitState.Editing -> when (intent) {
SubmitIntent.Submit -> submitFromEditing()
SubmitIntent.EditAgain -> Unit
}
is SubmitState.Failed -> when (intent) {
SubmitIntent.EditAgain -> backToEditing()
SubmitIntent.Submit -> Unit
}
SubmitState.Validating,
SubmitState.Submitting,
is SubmitState.Submitted -> Unit
}
}
到这里,MVI 完成了第二件事:把状态流转显式化。
它不是只问“收到什么 Intent”,还会问“当前处在什么 State”:
Intent + 当前 State -> 下一个 State / Effect
这才是 MVI 比状态驱动型 MVVM 多出来的结构。
四、MVI 带来了什么收益
结构讲完后,再来看收益就清楚很多。
1. ViewModel 不容易变成函数仓库
UI 不再到处调用 refresh()、retry()、deleteOrder()、submit(),而是统一发送 Intent。ViewModel 的公开入口更少,页面支持哪些行为也更集中。
2. 复杂状态更容易守住合法流转
sealed UiState 只能保证“状态结果互斥”,比如不会同时是 Submitting 和 Submitted。但 MVI 进一步要求你写清楚:在 Submitted 状态下再次收到 Submit,到底应该忽略、报错,还是重新提交。
3. 日志、测试和回放更自然
这个收益最容易被低估,因为很多人会说:MVVM 也能打日志,为什么非要 MVI?
关键差异在于:MVVM 想回放,必须先把函数调用翻译成日志,再把日志翻译回函数;MVI 的 Intent 本来就是可以保存、可以重放的行为数据。
假设线上出现一个 Bug,用户的操作顺序是:
进入页面 -> 删除订单 123 -> 加载下一页 -> 重试
在 MVVM 里,用户行为通常表现为一次次函数调用:
viewModel.loadOrders()
viewModel.deleteOrder("123")
viewModel.loadNextPage()
viewModel.retry()
如果你想回放这串操作,就必须额外做三件事:
- 在每个函数里手动打出结构化日志。
- 约定日志格式,比如
"DeleteOrder:123"。 - 再写一个解析器,把日志重新映射回具体函数。
最后你大概率会写出这样的胶水代码:
fun replay(action: String) {
val parts = action.split(":")
when (parts[0]) {
"LoadOrders" -> loadOrders()
"DeleteOrder" -> deleteOrder(parts[1])
"LoadNextPage" -> loadNextPage()
"Retry" -> retry()
}
}
这段代码能跑,但它也把问题暴露得很明显:为了让 MVVM 支持回放,你其实是在 ViewModel 外面手写了一个弱化版的 Intent 分发器。
MVI 里,同一个行为天然就是数据:
val intents = listOf(
OrderIntent.LoadOrders,
OrderIntent.DeleteOrder("123"),
OrderIntent.LoadNextPage,
OrderIntent.Retry
)
这串 Intent 不只是日志,它本身就是页面的输入。
所以记录时可以直接记录:
logger.log(intent)
viewModel.onIntent(intent)
测试时也可以直接回放:
intents.forEach(viewModel::onIntent)
这就是“更自然”的意思:MVI 不需要把日志再翻译回行为,因为 Intent 本来就是行为。
当然,真实回放还要处理接口数据、时间、随机数、缓存等外部依赖。但至少在“用户操作序列”这一层,MVI 已经天然给了你一个可记录、可序列化、可测试的输入模型。
五、Effect 不要塞进 State
无论你用 MVVM 还是 MVI,都要注意一个常见坑:一次性事件不要放进持久状态里。
页面状态是可以重复渲染的,比如 Loading、Success、Error。
但 Toast、导航、弹窗、震动提示这类行为,通常只应该消费一次。它们更适合作为 Effect:
sealed interface OrderEffect {
data class ShowToast(val message: String) : OrderEffect
data class NavigateToDetail(val orderId: String) : OrderEffect
}
ViewModel 里用 Channel 或 SharedFlow 发送:
private val _effect = Channel<OrderEffect>(Channel.BUFFERED)
val effect = _effect.receiveAsFlow()
private fun openDetail(orderId: String) {
_effect.trySend(OrderEffect.NavigateToDetail(orderId))
}
Compose 里用 LaunchedEffect 消费:
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is OrderEffect.ShowToast -> showToast(effect.message)
is OrderEffect.NavigateToDetail -> navController.navigate("detail/${effect.orderId}")
}
}
}
这就是常见的三件套:
| 类型 | 作用 |
|---|---|
State |
当前页面长什么样 |
Intent |
用户或系统发生了什么 |
Effect |
只消费一次的副作用 |
六、2026 年的新变量:工具链正在影响架构选型
以前讨论 MVVM 和 MVI,很容易停留在“代码风格偏好”上。但到了 2026 年,工具链本身已经变了,这些变化会实实在在影响架构选型。
1. Compose 让状态驱动成为默认写法
在 XML + ViewBinding 时代,很多页面是“命令式更新 UI”:请求成功后手动 showContent(),失败后手动 showError()。
但 Compose 的心智模型天然更接近:
State -> UI
UI 不应该到处手动改控件,而是根据当前 UiState 重新渲染。这也是为什么现代 MVVM 越来越像 MVI 的一部分:它们都倾向于用单一 UiState 驱动页面。
所以在 Compose 项目里,状态驱动型 MVVM 已经是很好的默认起点。简单页面没有必要为了“像 MVI”而额外引入 Intent、Effect、状态转移表。
2. 官方架构建议让 UDF 成为共识
现在 Android 官方架构文档里反复强调 UI state、事件上行、状态下行这套模式。换句话说,UDF 已经不是 MVI 圈子里的专属术语,而是现代 Android 的基本共识。
这会让 MVVM 和 MVI 的边界变得更细:
- 状态驱动型 MVVM:可以做到
UiState -> UI,也可以做到事件回到 ViewModel。 - MVI:进一步要求事件本身也被建模成
Intent,并且通过统一入口处理。
所以不要再把“用了 UDF”当成“用了 MVI”。真正的区别还是:输入端有没有被数据化、收口和约束。
3. KMP 让纯 Kotlin 契约更值钱
如果项目只是 Android 单端,MVVM 和 MVI 都可以工作。
但如果你要做 KMP,把业务逻辑共享给 Android 和 iOS,MVI 的模型会更自然。
原因很简单:UiState、Intent、Effect 都是纯 Kotlin 数据契约,不依赖 Android 的 Context、Activity、Fragment 或 NavController。
共享层负责:
Intent -> 状态流转 -> UiState / Effect
平台层负责:
Android Compose / iOS SwiftUI -> 渲染 UiState,发送 Intent,消费 Effect
这样业务规则可以沉到共享层,平台层只保留 UI 和平台能力。对于多端项目,这通常比在两端各写一套 ViewModel 更容易保持一致。
4. Compose Multiplatform 会继续放大这个趋势
如果项目开始使用 Compose Multiplatform,UI 层也有机会进一步共享。这时架构选型会更偏向“纯状态 + 纯行为”的模型。
因为跨端共享最怕的是平台对象混进业务逻辑里。一旦 ViewModel 里到处都是 Context、导航对象、生命周期对象,共享成本就会迅速上升。
MVI 的 UiState / Intent / Effect 拆分,天然会提醒你:
UiState放稳定页面状态。Intent放用户行为。Effect放一次性副作用。- 平台能力留在平台层消费。
这不是说 Compose Multiplatform 项目必须用 MVI,而是说:越想共享 UI 和业务逻辑,越需要清晰的数据契约。
5. 测试和 AI 调试也更偏爱结构化输入
还有一个容易被忽略的新变量:测试和 AI 工具越来越依赖结构化信息。
如果你的页面行为散落在一堆 public 函数里,测试和排查问题时需要先理解“应该按什么顺序调用这些函数”。但如果页面入口本来就是 Intent,那么测试用例、日志分析、甚至 AI 辅助排查都更容易围绕一串行为数据展开。
这也是为什么复杂页面更适合 MVI:它不是让代码变少,而是让页面行为更容易被记录、分析和复现。
七、推荐的 2026 年工程写法
不要在全项目里只押一个架构。更实际的做法是分层使用。
1. 默认用状态驱动型 MVVM
适合大多数普通页面:
sealed interface ProfileUiState {
data object Loading : ProfileUiState
data class Success(val profile: Profile) : ProfileUiState
data class Error(val message: String) : ProfileUiState
}
class ProfileViewModel : ViewModel() {
private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
val uiState = _uiState.asStateFlow()
fun loadProfile() {
// load profile
}
fun retry() {
loadProfile()
}
}
它足够轻,也能避免多数非法状态。
2. 复杂页面升级到 MVI
当页面有大量交互、复杂状态流转,或者需要回放测试时,再引入完整 MVI:
sealed interface OrderIntent {
data object LoadOrders : OrderIntent
data object Retry : OrderIntent
data object LoadNextPage : OrderIntent
data class SelectOrder(val orderId: String) : OrderIntent
data class DeleteOrder(val orderId: String) : OrderIntent
}
sealed interface OrderUiState {
data object Loading : OrderUiState
data class Success(
val orders: List<Order>,
val selectedOrderIds: Set<String>,
val isLoadingNextPage: Boolean
) : OrderUiState
data class Error(val message: String) : OrderUiState
}
sealed interface OrderEffect {
data class ShowToast(val message: String) : OrderEffect
data class NavigateToDetail(val orderId: String) : OrderEffect
}
class OrderViewModel : ViewModel() {
private val _uiState = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val uiState = _uiState.asStateFlow()
private val _effect = Channel<OrderEffect>(Channel.BUFFERED)
val effect = _effect.receiveAsFlow()
fun onIntent(intent: OrderIntent) {
when (intent) {
OrderIntent.LoadOrders -> loadOrders()
OrderIntent.Retry -> loadOrders()
OrderIntent.LoadNextPage -> loadNextPage()
is OrderIntent.SelectOrder -> selectOrder(intent.orderId)
is OrderIntent.DeleteOrder -> deleteOrder(intent.orderId)
}
}
private fun loadOrders() {
// load orders
}
private fun loadNextPage() {
// load next page
}
private fun selectOrder(orderId: String) {
// update selected state
}
private fun deleteOrder(orderId: String) {
// delete order
}
}
这个模板展示的是 MVI 的基本骨架。真实复杂页面里,不是所有 Intent 都应该无条件执行;涉及状态约束的行为,应该在对应 handler 里结合当前 uiState 判断是否合法,比如只允许在 Success 状态下加载下一页、选择订单或删除订单。
Compose 层保持简单:观察 State,发送 Intent,消费 Effect。
@Composable
fun OrderScreen(
viewModel: OrderViewModel,
navController: NavController
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is OrderEffect.ShowToast -> showToast(effect.message)
is OrderEffect.NavigateToDetail -> {
navController.navigate("detail/${effect.orderId}")
}
}
}
}
when (val state = uiState) {
OrderUiState.Loading -> LoadingScreen()
is OrderUiState.Error -> ErrorScreen(
message = state.message,
onRetry = { viewModel.onIntent(OrderIntent.Retry) }
)
is OrderUiState.Success -> OrderList(
orders = state.orders,
selectedOrderIds = state.selectedOrderIds,
onOrderClick = { orderId ->
viewModel.onIntent(OrderIntent.SelectOrder(orderId))
},
onLoadNextPage = {
viewModel.onIntent(OrderIntent.LoadNextPage)
}
)
}
}
八、最后的选型原则
不要问“MVI 和 MVVM 谁更好”,而要问下面这几个问题:
- 这个页面的状态是否复杂到容易出现非法组合?
- 用户行为是否多到 ViewModel 会暴露一堆 public 函数?
- 线上问题是否需要通过操作序列回放来复现?
- 这块业务是否要共享到 KMP 多端?
- 团队是否愿意用更多模板代码换更强约束?
如果大多数答案是否定的,用 MVVM。
如果大多数答案是肯定的,用 MVI。
最终建议可以压成一句话:
普通页面,用状态驱动型 MVVM;复杂交互、强状态约束、可回放、多端共享,用 MVI。
这才是 2026 年 Android 架构选型里真正务实的答案。

浙公网安备 33010602011771号