八年前的开发者还在纠结MVP和MVC。今天,Android团队官方推荐的MVVM,已经让无数人陷入另一种泥潭。

一个典型的ViewModel长什么样?五个MutableStateFlow属性挤在一起:name、email、isLoading、error,还有没写出来的toast触发器。加载状态散落在三个地方,每个方法都复制粘贴一遍try-catch-finally。最讽刺的是,ViewModel并不真正"拥有"它的副作用——viewModelScope才是那个隐形的老板。

打开网易新闻 查看精彩图片

MVI曾被当作解药。把状态塞进密封接口,Loading、Loaded、Error各司其职,看起来纯净多了。但MVI对副作用保持沉默。有人写middleware,有人用Channel,有人在LaunchedEffect里打补丁。每个团队都在重新发明轮子,而且轮子形状各不相同。

问题到底卡在哪?MVVM的状态太分散,MVI的副作用太暧昧。两者都没解决一个核心矛盾:如何在不牺牲可测试性的前提下,让异步操作和状态变更同频共振。

Reduce & Conquer模式试图给出答案。核心洞察很简单:reducer不该只返回状态,它应该返回一次完整的"过渡"(Transition)。这个过渡包含三样东西——新状态、一次性事件(导航、提示)、以及真正需要执行的副作用(Effect)。

Effect被提升为一等公民,有三种形态。Stream订阅一个Flow,持续产生命令;Action执行单次异步操作,成功或失败都映射为命令;Cancel则按key取消,防止内存泄漏。关键是,这些Effect不直接在reducer里执行,它们只是声明。真正的执行层在别处,测试时可以直接跳过。

看一个实际例子。当用户点击更新资料,reducer返回的新状态把isLoading设为true,同时附带一个Action Effect。这个Effect的key是"update_profile",block里调用profileService.update,失败时通过fallback映射为ProfileError命令。服务返回后,reducer再次响应ProfileUpdated命令,把isLoading关掉,并抛出一个NavigateBack事件。

整个流程里,reducer始终是纯函数。副作用被推迟到执行层,状态变更和事件发射都在一次函数调用内完成。测试时,你只需验证Transition的构成;运行时,执行层保证Effect的生命周期和取消语义。

这不是又一个架构框架,而是一种对问题域的重新划分。状态、事件、副作用三者解耦,但又在同一个过渡对象里保持关联。对于那些被MVVM的散乱和MVI的模糊折磨过的开发者,这种显式化或许值得一试。