文章目录
目标:描述清楚数据流向,MVC/MVP的弊端,MVVM,MVI的差异
- 所谓的MVVM是双向数据流,MVI是单向数据流。其实是基于MVVM用了DataBinding,将ViewModel数据绑定到xml中。解耦了这一层,本质也是单向数据
- 以登录为例,比如登录后有成功/失败/网络异常等,MVVM框架写多个livedata(其实也可以写一个loginresult)去监听,而对于MVI就全部封装在一个State类里。
MVI 中 View 不会直接赋值给 ViewModel,View 只发送 Intent,所有状态变更都由 ViewModel 内部基于 Intent 来产生,UI 只负责订阅 State 渲染。
而普通 MVVM 架构本身没有强制约束,语法上 View 可以直接修改 ViewModel 内部 MutableLiveData;虽然好的工程实践会避免这种写法,但这只是团队规范,不是架构强制。所以 MVVM 可以做到单向,但做不到 MVI 那种强约束的单向闭环。
1.MVC(Model‑View‑Controller)
架构角色
- Model:数据层,业务实体、网络、数据库,负责数据获取与存储
- View:视图,Activity / XML,负责 UI 渲染、接收用户交互
- Controller:控制器,Android 原生没有独立 Controller 类,由 Activity 兼任,接收 View 事件,调用 Model,拿到结果再驱动 View 刷新
架构简易关系
View ↔ Controller ↔ Model
Android 原生 MVC 最大问题:Activity 既做 View 又做 Controller,大量业务逻辑写在 Activity,页面越来越重,视图和控制器强耦合。
优点
- 简单直观,上手成本低,原生 Android 天然支持
- Model 完全独立,可复用
缺点
- Activity 职责过载,巨型 Activity,单元测试困难
- View 持有 Controller,Controller 又持有 View,双向耦合
- View 强依赖 Android 框架,难以脱离设备做单元测试
适用场景:小型简单页面、Demo、老项目维护。
2.MVP(Model‑View‑Presenter)
核心目标:解耦 Activity,把业务逻辑抽离到 Presenter
架构角色
- Model:数据层,不变
- View:接口,由 Activity/Fragment 实现 View 接口,只做 UI 操作
- Presenter:中间人,持有 View 接口引用 ,处理业务逻辑,调用 Model,回调更新 View
架构简易关系
View <--> Presenter <--> Model
View只通过接口和Presenter交互
优点
- Activity/Fragment 轻量化,只负责 UI;业务全部下沉 Presenter
- View 抽象成接口,Presenter 不依赖 Android SDK,可做纯 JVM 单元测试
- 职责清晰,适合中等复杂度业务
缺点
- 接口爆炸:每一个页面要定义一套 View 接口,模板代码多
- 内存泄漏风险:Presenter 持有 View 引用,页面销毁忘记置空会泄漏 Activity
- 单向数据流不彻底,容易出现 Presenter 直接操作 View、状态分散
- 状态需要自己维护,配置变更(屏幕旋转)需要手动保存恢复
适用场景:中大型传统业务页面,老项目重构;现在新项目逐步被 MVVM/MVI 替代。
3.MVVM(Model‑View‑ViewModel)
核心:数据驱动 UI,观察者模式,消除 Presenter,ViewModel 持有可观察数据,View 订阅数据变化自动刷新
架构角色
- Model:数据层
- View:Activity/Fragment,只负责观察数据、事件转发
- ViewModel:保存页面状态、业务逻辑,提供 LiveData/StateFlow 可观察数据;不持有 View 引用
架构简易关系
View(订阅) ← ViewModel ← Model
View的事件单向传入ViewModel
优点
- ViewModel 不持有 View 引用,从根源规避 MVP 的内存泄漏
- 数据驱动 UI,UI 跟随数据自动更新,减少大量手动调用 View 的模板代码
- 屏幕旋转配置变更,ViewModel 自动保留状态
- Jetpack 原生支持,Android 官方主推
缺点
- 状态分散:多个 LiveData/StateFlow,页面状态散落在多个变量,状态一致性维护成本高
- 调试难度上升:数据变化链路间接,不容易追踪状态变更来源
- LiveData 有生命周期感知,但只能在主线程分发;复杂页面容易出现多源数据冲突(例如 MediatorLiveData 踩坑)
适用场景:绝大多数现代 Android 业务,大部分 App 主流选型。
4.MVI(Model‑View‑Intent)
注意:MVI 里的 Intent 不是 Android 组件 Intent,代表用户意图;核心:单向数据流、单一数据源、不可变状态
架构角色
- Model:业务数据
- View:只做两件事:发送用户意图、渲染页面状态
- ViewModel:接收 View 发送的 Intent,执行业务,输出唯一的 UiState(不可变数据类)给 View;View 只渲染这个 UiState
架构简易关系
View → Intent → ViewModel → UiState → View
单向闭环数据流
优点
- 单一 UiState,页面所有状态收敛到一个数据类,状态可回溯,适合复杂交互页面
- 单向数据流,逻辑可预测,易于复现 bug、便于单元测试
- 天然适配协程 + StateFlow,适合 AI 对话、流式输出、富交互这类复杂状态场景
缺点
- 模板代码量大,每个页面要定义 Intent、UiState、Event,代码量显著上涨
- 不可变对象频繁复制,对象创建开销;简单页面会过度设计
- 一次性事件(Toast、弹窗)处理麻烦,需要额外封装 Event 包装类,容易踩坑
适用场景:复杂交互页面:AI 对话、表单多状态、长列表复杂业务;中台模块;项目团队统一规范、强质量要求项目。

面试加分表述(字节 5 年视角)
- 没有绝对最优架构,看业务复杂度和团队;简单页面强行上 MVI 属于过度设计。
- MVP 最大痛点是 View 接口爆炸、内存泄漏;MVVM 要警惕状态碎片化;MVI 最大坑是一次性事件处理、对象开销。
- 我实际项目中,普通业务页用 MVVM;AI 助手这种多状态、流式输出的复杂模块,采用 MVI 单向数据流做状态收敛。
文字版架构图
- MVC
用户操作 → View → Controller (Activity) → Model;Model 返回数据直接回写 View;View 和 Controller 互相持有。 - MVP
View (Activity 实现 View 接口) ↔ Presenter → Model;Presenter 通过 View 接口回调 UI,Presenter 持有 View 接口。 - MVVM
View 订阅 ViewModel 的 LiveData/StateFlow;View 事件回调给 ViewModel;ViewModel 调用 Model,输出可观察数据,ViewModel 不持有 View。 - MVI
View发出Intent → ViewModel处理Intent调用Model → 生成唯一不可变UiState → View渲染UiState,完整单向闭环。
补充理解MVI的side-effect
1. Side-Effect 核心定义
- 在 MVI 单向数据流架构中,所有页面输出分为两类:可持久化 UI 状态(UiState)与一次性副作用(Side-Effect)。
- Side-Effect(副作用):指业务执行过程中产生的单次消费、不可重放、不持久化的页面行为,该行为不参与页面 UI
渲染,仅用于完成单次交互动作,消费完成后立即失效,不保留任何状态记录。

2. Side-Effect 产生的根本原因
2.1 架构层面根源:单一 UiState 的局限性
MVI 核心规范是单一数据源、UI 唯一由 UiState 驱动,但页面业务无法只靠「静态渲染状态」完成,必然存在大量「动态单次行为」。
如果所有业务输出全部塞入 UiState,会导致状态与行为混淆,引发系统性 Bug。
2.2 技术层面根源:StateFlow 可重放特性
Jetpack 官方 ViewModel 生命周期机制 + StateFlow 特性:
- ViewModel 独立于页面生命周期,屏幕旋转、Fragment 重建不会销毁 ViewModel
- StateFlow 默认缓存最新值,页面重连订阅时会自动回放最新状态
若将一次性行为存入 UiState(StateFlow),页面重建后会重复回放旧的行为事件,造成 Toast 重复弹出、页面重复跳转等严重交互 Bug。
2.3 业务层面根源:状态与行为的本质冲突
- UiState 是「结果」:是页面的静态快照,允许重复渲染,幂等性强
- Side-Effect 是「动作」:是业务的动态过程,不允许重复执行,无幂等性
处理方案
使用 SharedFlow 或 Channel 来处理一次性事件(如导航、Toast)
SharedFlow
定义effect密封类,通过监听MutableSharedFlow处理
// 1. 定义副作用
sealed interface HomeEffect {
data class Toast(val msg: String) : HomeEffect
data class Navigate(val route: String) : HomeEffect
object ScrollBottom : HomeEffect
}
// 2. ViewModel
class HomeViewModel : ViewModel() {
private val _effect = MutableSharedFlow<HomeEffect>(
replay = 0,
extraBufferCapacity = 3,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val effect: SharedFlow<HomeEffect> = _effect
fun submit() {
viewModelScope.launch {
runCatching { repo.submit() }
.onSuccess { _effect.tryEmit(HomeEffect.Navigate("detail")) }
.onFailure { _effect.tryEmit(HomeEffect.Toast("提交失败")) }
}
}
}
// 3. Fragment 收集
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(STARTED) {
viewModel.effect.collect { e ->
when (e) {
is HomeEffect.Toast -> toast(e.msg)
is HomeEffect.Navigate -> nav(e.route)
HomeEffect.ScrollBottom -> rv.scrollToEnd()
}
}
}
}
- 补充SharedFlow的参数
StateFlow 用来存页面 UI 状态,自带最新值、自动去重;SharedFlow 用来发一次性事件,配 replay=0 就不带黏性,不会页面重建重复弹 Toast。两者都是热流。
val sharedFlow = MutableSharedFlow<String>(
replay = 0, // 新订阅者,回放多少条历史数据
extraBufferCapacity = 0, // 额外缓冲区大小
onBufferOverflow = BufferOverflow.SUSPEND // 缓冲区满了的策略
)
Channel
Channel 主要用于消息不允许丢失、需要背压控制的场景,例如 IM 消息、设备硬件事件、底层推送。send 是挂起函数,生产者可以等待消费者处理完成。
对于普通 UI 的 Side‑Effect,比如 Toast、页面跳转,不建议使用 Channel,容易发生协程阻塞、关闭异常,优先采用replay=0的 SharedFlow。
// 1.定义副作用密封类
sealed class DeviceSideEffect {
// 设备离线事件,不能丢失
object DeviceDisconnect : DeviceSideEffect()
data class ShowImportantAlert(val msg: String) : DeviceSideEffect()
object NavigateToDeviceSetting : DeviceSideEffect()
}
class DeviceViewModel : ViewModel() {
/**
* Channel.UNLIMITED:队列无上限,事件不会丢失
* 注意UNLIMITED风险:事件疯狂涌入会持续占用内存
* 普通业务可选用 Channel.BUFFERED 设置固定缓冲区
*/
private val _sideEffectChannel = Channel<DeviceSideEffect>(Channel.UNLIMITED)
// 转为Flow对外暴露,只读
val sideEffectFlow = _sideEffectChannel.receiveAsFlow()
fun handleDeviceStatusChange() {
viewModelScope.launch {
// 底层设备回调,重要事件不能丢失
_sideEffectChannel.send(DeviceSideEffect.DeviceDisconnect)
}
}
override fun onCleared() {
super.onCleared()
// ViewModel销毁,必须关闭Channel,防止继续发送抛出异常
_sideEffectChannel.close()
}
}
技术选型对比
为啥绝大多数场景选SharedFlow
- Channel(信箱):
你往信箱塞消息 (send),如果对面(Fragment)不在家(APP 退后台、页面不可见),信箱满了,塞快递这个动作就卡住不动了,后面你的业务代码全部被卡住等待。
页面后台驻留时 send 会阻塞生产者协程
send 是挂起函数:消费者没接收,生产者就原地等着。
适合场景:重要消息丢不起,愿意等对方接收,比如设备断开、IM 消息。
- 普通 UI 副作用(Toast、跳转、滚动)是什么?
用户退到后台,这个 Toast 本来就没必要弹。
如果用 Channel,页面退后台,你想弹个 Toast,结果页面没在看,send()直接把 ViewModel 里面的协程卡住,网络、设备业务全部暂停,这就是灾难。
UI 副作用:丢了就丢了,绝不允许把核心业务给卡住。
- SharedFlow (replay=0)(广播喇叭):
用户不在家,我喊一声,没人听就算了,发送动作不会卡住我自己,该干啥继续干。
刚好匹配 Toast、跳转:后台来了就直接放弃,绝不阻塞主业务。
3688



被折叠的 条评论
为什么被折叠?



