能告诉我:你为什么用 MVI 吗?反正我不理解!

MVI vs MVVM 讨论记录

我对 MVI 一直不理解,所以和 Claude Code 有了以下对话,不纠结是黑是白,只加深两种模式之间的理解。

第一轮:MVVM 和 MVI 有什么本质的区别

问题

我想知道在 Android 开发中:MVVM 和 MVI 有什么本质的区别

回答

MVVM vs MVI 的本质区别

两者都是基于单向数据流的架构,但在状态管理粒度上有根本差异:

MVVM(Model-View-ViewModel)

  • 状态分散 :ViewModel 中有多个独立的 StateFlow/LiveData
  • 事件处理 :View 直接调用 ViewModel 的方法(viewModel.login()
  • 状态更新:各字段独立更新,可能出现状态不一致
kotlin 复制代码
class LoginViewModel : ViewModel() {
    val username = MutableStateFlow("")
    val loading = MutableStateFlow(false)
    val error = MutableStateFlow<String?>(null)
​
    fun login() { ... }
}

MVI(Model-View-Intent)

  • 状态集中:单一不可变 State 对象描述整个屏幕状态
  • 事件处理:View 发送 Intent(密封类),ViewModel 通过 Reducer 处理
  • 状态更新:每次生成全新的 State,保证原子性一致
kotlin 复制代码
data class LoginState(val username: String = "", val loading: Boolean = false, val error: String? = null)
​
sealed class LoginIntent {
    data class TypeUsername(val text: String) : LoginIntent()
    object Submit : LoginIntent()
}
​
// Reducer: (State, Intent) -> State

本质差异总结

维度 MVVM MVI
状态 多个可变流 单一不可变对象
事件 方法调用 Intent 密封类
可预测性 中等(状态可能不同步) 高(状态原子更新)
样板代码
调试 需要跟踪多个流 可回放 Intent 序列

实际选择:简单页面用 MVVM 就够了;复杂交互(多状态联动、需要时间旅行调试)用 MVI 更安全。Compose 本身的单向数据流天然更亲近 MVI。


第二轮:直接调用函数 vs 发射 Intent 间接调用

问题

深入这个话题:尤其关于事件:直接调用函数,和发射 Intent 间接调用 有什么本质区别和改变吗?是不是过度设计

回答

直接调用 vs Intent 分发:本质区别在哪?

先说结论:大多数场景下,Intent 确实是过度设计

两者在运行时的执行路径几乎一样:

less 复制代码
// MVVM:直接调用
button.onClick { viewModel.login(username, password) }
​
// MVI:发送 Intent → reduce
button.onClick { viewModel.dispatch(LoginIntent.Submit(username, password)) }
// 然后内部: when(intent) { is Submit -> doLogin(intent.username, intent.password) }

最终都是调同一个函数。Intent 只是多了一层间接。

Intent 真正带来的区别

1. 可序列化/可记录

Intent 是数据类,可以被记录、回放、序列化:

less 复制代码
// 可以做到:记录用户操作序列
val history = listOf(
    TypeUsername("john"),
    TypePassword("123"),
    Submit
)
// 回放调试、上报 crash 现场

直接方法调用做不到这一点------调用完就没了,没有痕迹。

2. 统一入口 = 统一拦截

所有操作经过同一个 dispatch,可以在一处加日志、节流、权限校验:

kotlin 复制代码
fun dispatch(intent: Intent) {
    log(intent)                    // 统一日志
    if (rateLimiter.check()) return // 统一节流
    reduce(currentState, intent)
}

MVVM 中每个方法各管各的,想统一拦截要靠 AOP 或手动在每个方法里加。

3. 强制穷举

when(intent) 配合 sealed class,编译器强制你处理每种情况。MVVM 的方法则是"有就调、没有就忘了"。

什么时候不值得

如果你不需要:

  • 操作回放/时间旅行调试
  • 统一拦截层
  • 严格的状态机建模

那 Intent 层就是纯粹的样板代码。一个表单页面有 save()validate()clear() 三个方法,直接调用完全够了。把它们包成 Intent 密封类只是增加了跳转层级,没带来实际收益。

实际建议

场景 选择
普通 CRUD 页面 直接方法调用
复杂多步流程(如支付、注册向导) Intent 有价值
需要操作日志/crash 还原 Intent 有价值
团队很大、需要严格规范 Intent 降低沟通成本

不要因为"MVI 更现代"就选它。 架构服务于问题,不是反过来。


第三轮:在每个函数中添加日志不也能实现相同效果吗

问题

还是无法理解!!! 在每个函数中添加日志不是也能实现相同的效果吗

回答

你说得对,确实能实现相同效果。让我更诚实地分析这个问题。

逐条拆解:Intent 的"优势"是否站得住脚

统一日志 → 每个函数加也行

kotlin 复制代码
// MVI 方式
fun dispatch(intent: Intent) {
    log(intent) // 一处搞定
    reduce(state, intent)
}
​
// MVVM 方式
fun login() { log("login"); ... }
fun register() { log("register"); ... }
fun logout() { log("logout"); ... }

差别只是"一处写 vs 多处写"。 如果你有 5 个方法,多写 5 行 log()。这不是架构差异,是代码量差异,而且很小。

操作回放 → 你真的需要吗?

99% 的 Android 应用从来不会用到"回放用户操作序列"这个功能。这是 Redux DevTools 带火的概念,在前端调试复杂 SPA 时有用,但移动端几乎没有落地场景。

编译器穷举 → 直接方法调用根本不存在遗漏问题

when(sealed class) 强制穷举是针对"分发"模式的。你直接写方法,压根就不存在漏处理的可能------方法写了就是处理了,没写就是没这个功能。

所以本质是什么?

Intent 模式的真正价值只有一个:把"做什么"和"怎么做"在代码结构上解耦。

kotlin 复制代码
// Intent 定义了"做什么"(纯数据)
sealed class Intent {
    object Login : Intent()
}
​
// Reducer 定义了"怎么做"(纯函数)
fun reduce(state: State, intent: Intent): State

这在状态极其复杂、多个 Intent 交叉影响同一份 State时有意义------因为所有状态变更都经过同一个 Reducer,你可以精确控制状态转换,避免竞争。

但对于"一个按钮调一个方法改几个字段"的场景,这种解耦就是无意义的抽象层。

最终判断

对大多数 Android 页面来说,MVI 的 Intent 模式就是过度设计。 它从 Redux 继承过来,而 Redux 的设计背景是浏览器中不可变状态管理------那是 JavaScript 没有原生响应式系统时的产物。Compose 本身的 State + StateFlow 已经解决了核心问题。

用 MVVM 直接调方法,简洁明了,没什么可心虚的。


第四轮:Reducer 真能控制异步状态吗?

问题

这在状态极其复杂、多个 Intent 交叉影响同一份 State时有意义------因为所有状态变更都经过同一个 Reducer,你可以精确控制状态转换,避免竞争。

我想知道你说的上面这句话是否成立???

比如 ViewModel 中函数大多数是请求数据,这已经是异步了,通过一个函数来控制,真能控制吗? 如果这样说:直接调用函数,虽然会有多处调用,但是调用都是在 Main 线程中,所以实际上这些调用虽然没有通过同一个函数控制,但是实际上还是有一个明确的先后顺序的

回答

你的质疑非常到位。

Reducer "精确控制状态转换"------这个说法的真实性

你说的完全正确:Main 线程本身就是串行的

kotlin 复制代码
// MVVM:两个按钮点击
fun loadProfile() { _state.value = state.value.copy(loading = true); fetchProfile() }
fun loadOrders() { _state.value = state.value.copy(loading = true); fetchOrders() }

这两个调用都在 Main 线程,不可能同时执行。先点哪个就先执行哪个,不存在竞争。

MVI 的 Reducer 也控制不了异步

kotlin 复制代码
fun reduce(state: State, intent: Intent): State = when (intent) {
    is LoadProfile -> state.copy(loading = true) // 同步部分可控
    is ProfileLoaded -> state.copy(profile = intent.data) // 但这个 intent 什么时候来?不可控
    is LoadOrders -> state.copy(loading = true)
    is OrdersLoaded -> state.copy(orders = intent.data)
}

ProfileLoadedOrdersLoaded 哪个先回来,取决于网络,Reducer 管不了。所以"精确控制状态转换"在异步场景下是伪命题------无论 MVVM 还是 MVI,真正的难点(异步回调的时序)两者同样无法在结构上保证。

那 Reducer 到底在什么场景下有实际意义?

纯同步、多字段联动的状态机场景:

kotlin 复制代码
// 比如一个复杂表单,字段之间有约束关系
fun reduce(state: FormState, intent: FormIntent): FormState = when (intent) {
    is SelectCountry -> state.copy(
        country = intent.country,
        city = "",           // 换国家要清城市
        zipFormat = getFormat(intent.country) // 联动邮编格式
    )
    ...
}

这里一个操作要同时改 3 个字段且有逻辑依赖。放在 Reducer 里能保证原子性------一次 copy 产出完整新状态。

但 MVVM 用一个方法也能做到:

kotlin 复制代码
fun selectCountry(country: Country) {
    _state.update {
        it.copy(country = country, city = "", zipFormat = getFormat(country))
    }
}

效果完全一样。 只是 MVI 用 sealed class 强制了这种写法,而 MVVM 靠开发者自觉。

总结

论点 是否成立
"Reducer 能精确控制异步状态" 不成立,异步时序两者都控制不了
"Main 线程调用本身就有序" 成立,你说得对
"Reducer 保证同步状态原子性" 成立,但 StateFlow.update {} 也能做到
MVI 比 MVVM 更安全 在异步为主的 Android 开发中,差别微乎其微

我之前说的那句话不够严谨。MVI Reducer 在实际 Android 开发中的"控制优势"被大幅夸大了。


第五轮:MVI 集中 State 对 Compose 重组的影响

问题

继续聊 MVI 中的把状态 State 都集中到一个类中。这种方式是否是一种好的方式:假如我有两种状态 a,b ;MVI 中 state(a,b);MVVM 中两个状态是独立的。那来聊聊当其中一个状态a 发生变化时,对重组的影响。 a 实际用在 Composable A 中;b 实际用在 Composable B 中;在我的理解中,如果采用的MVVM,那么a变化时只会使 Composable A 重组,B不会重组;但是如果采用的MVI,因为整个 state 类都变了,而 Composable A B 都会受影响,都需要重组

回答

你的理解完全正确,这正是 MVI 单一 State 的一个实际缺陷。

MVI 集中 State 对 Compose 重组的影响

验证你的判断

kotlin 复制代码
// MVI:单一 State
data class ScreenState(val a: String, val b: Int)
​
@Composable
fun Screen(state: ScreenState) {
    ComposableA(state.a)  // 读了整个 state
    ComposableB(state.b)  // 读了整个 state
}

a 变化 → ScreenState 是新对象 → Screen 重组 → A 和 B 都重新调用

虽然 Compose 有 skip 优化(如果 ComposableB 的参数 state.b 没变,且参数是 stable 的,编译器可能跳过其内部执行),但 Screen 这层的重组是跑不掉的。

kotlin 复制代码
// MVVM:独立 State
val a = stateFlow<String>()
val b = stateFlow<Int>()
​
@Composable
fun Screen(viewModel: VM) {
    val a by viewModel.a.collectAsState()
    val b by viewModel.b.collectAsState()
    ComposableA(a)
    ComposableB(b)
}

这里 a 变化 → 只有 aState 对象变了 → Compose 精确知道只有读取 a 的作用域需要重组。ComposableB 完全不受影响。

但有一个关键细节

如果 MVI 这样写,可以缓解问题:

kotlin 复制代码
@Composable
fun Screen(state: ScreenState) {
    // 用 remember + derivedStateOf 或直接传子字段
    ComposableA(state.a)  // 只传子字段,不传整个 state
    ComposableB(state.b)
}

因为 ComposableA(state.a)state.a 是 String(Stable + equals 比较),如果值没变,Compose 编译器插件会生成 skip 逻辑。所以 B 的内部不会真正执行 ,但 Screen 级别的重组开销仍然存在。

实际影响有多大?

场景 影响
State 字段少(<10个),子 Composable 参数都是基本类型/String 几乎无感,skip 优化兜底
State 字段多,子 Composable 接收 List/Map 等不稳定类型 明显劣化,每次都重组
高频更新(如拖拽坐标、动画帧) 严重劣化,MVVM 独立 State 明显更优

结论

MVI 的单一 State 和 Compose 的精细化重组机制是天然矛盾的。 Compose 的重组优化依赖"精确知道哪个 State 变了",而 MVI 把所有东西塞进一个对象,等于主动放弃了这种精确性。

MVVM 的独立 StateFlow 在 Compose 中反而是更优的模式------它与 Compose 的响应式模型完美对齐。


第六轮:总结

问题

所以我可以理解成,MVI 两种策略都有弊端。不如用 MVVM

回答

基本可以这样理解,但加一个限定更准确:

在 Compose(包括 CMP)的场景下,MVVM 是更务实的选择。

MVI 的核心设计(单一 State + Intent 分发 + Reducer)源自 React/Redux 生态,那个环境没有 Compose 这种编译器级别的精细重组。搬到 Compose 后,它的"集中状态"反而与框架的优化机制冲突,而"Intent 分发"在大多数业务场景下又没有比直接调方法好多少。

不过也不用完全否定 MVI------如果团队已经在用且运转良好,强行切换的成本也不值得。架构选择没有绝对对错,只有适不适合当前的技术栈和团队规模。

对你个人或新项目来说,Compose + MVVM(ViewModel 暴露多个独立 StateFlow + 直接方法调用)是最简洁高效的组合。

相关推荐
ai产品老杨2 小时前
深度解析:异构算力下的 AI 视频管理平台架构实现 (GB28181 / Docker / 源码交付)
人工智能·架构·音视频
荣月灵的小梅花2 小时前
Android 给广播接收器增加权限(permission)或signature签名权限
android
我滴老baby2 小时前
工具调用全景解析从Function Calling到MCP协议的完整实践
开发语言·人工智能·python·架构·fastapi
繁星蓝雨3 小时前
Qt多界面创建的优化问题(main函数或主界面中创建?)—————附带详细方法
c++·qt·架构·多界面管理
沐言人生3 小时前
ReactNative 源码分析4——ReactActivity之加载JSBundle
android·react native
搬砖的小码农_Sky3 小时前
AI Agent:OpenClaw的算法架构
人工智能·算法·ai·架构·人机交互·agi
砖厂小工4 小时前
Now In Android 精讲 10 - AGENTS.md:写给 AI Agent 的项目说明书
android
Jiude5 小时前
经验正在失去垄断性
人工智能·架构·设计
萑澈5 小时前
DeepSeek V4 架构演进全解析:混合注意力机制与流形约束残差优化深度研究报告
架构