当我们在谈MVP、MVVM、MVI的时候,我们到底在谈什么?
💡 前言
- 当我们谈论 MVP、MVVM、MVI 时,
- 我们在谈的是:
- UI 层的架构模式(Presentation Layer Architecture Pattern)
- 也就是「界面展示层的职责划分与数据流动方式」。
- 这是一种解耦合的数据基础架构。
📦 背景:我们为什么需要这些模式?
早期在开发 UI(无论是 Android、iOS、Web)时,常常出现这样的代码结构:
所有逻辑都在一个“巨大的控制器”或“Activity”或“ViewController”里。
这就导致了:
- UI 代码和业务逻辑严重耦合;
- 难以测试;
- 难以复用;
- 状态难以管理。
于是,架构模式的目的是:
- 让“UI层代码结构化”,
- 让“数据流动清晰”,
- 让“状态管理可控”。
MVC
- 最传统的方式是使用MVC。
- 因为在Android的View体系下,布局文件一般放在了xml中,而Activity/Fragment则作为Controller,去实现UI视图数据刷新的动作。但受限于xml布局文件的能力实在是有限,很多动作不得不放在Activity/Fragment中来实现,于是这个Controller在某种程度上,也承担了大量的View的任务。
- 这个时候如果把数据的获取方式依旧放在Activity/Fragment中,不可避免的是这里的内容会非常多,维护也将变得很为难。于是Google官方推荐大家使用MVP的方式去替代MVC。
MVP
- 一种用契约和接口来实现数据回调的结构
- MVP就是通过ViewRender和IPresenter的方式去把数据生成的动作放在了Presenter中,由Presenter去访问更上一层的Service,获取到相应的Data,再通过ViewRender把这个数据返回到View中,View可以在自己实现的接口根据数据去给视图资源刷新UI。
- 但是,这种方式在后期也暴露了它的缺点。网上对它的缺点讨论主要是围绕在Presenter依旧太大了,但我觉得网上说的那种Presenter承担的任务太重这个说法,并不是MVP的问题,这是因为数据控制层应该更加细分成为各个模块,比如Repository、UseCase等等,这个划分哪怕是去到MVVM也是要做这一块的工作的,这个需要开发者和开发团队按照具体的业务去划分详细的职责模块。
- 我个人认为,MVP被大家逐渐淘汰的原因,是因为Presenter和View之间的依赖注入必须由开发者手动去注入生命周期,这就取决于各个团队对MVP的认知和封装了。
- 很常见的一点就是,一些人以及一些团队对MVP的编写得过于简陋。
- 他们在封装MVP架构过程中,只是实现了数据回调,只实现了Presenter向View返回数据这一个功能,而忽略了生命周期的绑定。
- 这就导致,当一个View被销毁了,Presenter的请求还在继续,当Presenter向View返回数据的时候,View中的对象已经被销毁了。
- 这个时候在Java项目中就会出现NPE的情况,即使是在Kotlin项目中,能够通过空安全去避免一些NPE的情况,但也还是会出现内存泄漏以及ANR的情况。
- 另外,比较让人不适应的一点,就是它通过大量的接口去实现数据回调,这点是Java的魅力,但也是Java显得很笨重的原因,当要去找一个业务的具体实现的时候,这就变得很繁琐了。
- 所以为了避免参差不齐的MVP封装情况,Google在Jetpack中向开发者推出了强制注入生命周期的ViewModel,而且使用方式也是极其简单的。
为了避免把这篇文章拉得太长,新开了一篇文章,简略介绍了一下MVP的开发模式
想详细了解这一个的用法,去这里看
MVVM
- 一种通过观察者模式实现数据回调的单一数据源结构
- 核心结构(这个图是结合我的理解用ProcessOn画的,每个人理解可能会有不同的偏差,如果有不同看法,结合你的业务去理解会更好)
ViewModel的创建方式很简单,按照官方的推荐,注入相应的生命即可以获取到相应的ViewModel,当然它这里还涉及到工厂模式,这部分详细参见ViewModelProvider.Factory:
- 自己生命周期的
kotlin
private val viewModel: MyViewModel by lazy {
ViewModelProvider(
this,
MyViewModelFactory()
)[MyViewModel::class.java]
}
- 父容器生命周期的
kotlin
private val parentViewModel: RouteViewModel? by lazy {
parentFragment?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[RouteViewModel::class.java]
}
}
private val activityViewModel: ActivityViewModel? by lazy {
activity?.let {
ViewModelProvider(
it,
MyViewModelFactory()
)[ActivityViewModel::class.java]
}
}
ViewModel的实际编写和LiveData的使用
kotlin
class MyViewModel(
private val repository: Repository,
): ViewModel() {
private val _data: MutableLiveData<String> = MutableLiveData()
val data: LiveData<String> = _data
private val _errorMessage: MutableLiveData<Throwable> = MutableLiveData()
val errorMessage: LiveData<Throwable> = _errorMessage
private val _isRefresh: MutableLiveData<Boolean> = MutableLiveData()
val isRefresh: LiveData<Boolean> = _isRefresh
fun fetchData() {
_isRefresh.value = true
viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> {
_errorMessage.value = throwable
_isRefresh.value = false
}}) {
// Image repository.fetchData() run in background thread to fetch data.
// Warning: when you want to post something to livedata, you need to stay at UI Thread.
_data.value = repository.fetchData()
_isRefresh.value = false
}
}
}
class MyFragment: Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.isRefresh.observe(this) {
showHideSpinner(it)
}
viewModel.errorMessage.observe(this) {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}
viewModel.data.observe(this) {
updateUI(it)
}
viewModel.fetchData()
}
private fun updateUI(data: String) {
// updateUI
println(data)
}
}
- MVVM与MVP的不同,就是MVVM并不一定要采用双向绑定,受限于DataBinding双向绑定的能力实在是太弱了,在使用MVVM的时候,我个人更加喜欢使用单一数据源,这也是Google最推荐的方式。结合着LiveData这一个拥有生命周期注入的监听工具,ViewModel中的数据可以很方便返回给到View。
- 值得提及的一点是,我个人常用的单Activity多Fragment模式,使用parentFragment的生命周期去实现路由跳转是一个很好解决路由问题的方式。
- 当然不可避免的一点,当我们使用LiveData的时候,按照以往既定的想法,每一个LiveData会对应一个状态。像上面的示例,它就存在了刷新中、数据请求成功、数据请求失败着3个状态。 这就会导致在ViewModel中,LiveData变得异常多,我们俗称为LiveData爆炸。
MVI
- 一个对MVVM的稍许优化的新结构
- 为了避免出现LiveData爆炸,在MVVM开发的实际过程,我们可以使用一个枚举的方式,在Kotlin的角度,这个叫密封类。通过枚举的方式,我们可以把一个请求可能面对的多种状态列举出来,在LiveData实际去监听的时候,再通过switch/when的方式去拿到对应的每一种状态,然后实现各种状态下的UI刷新,例如updateUI、ToastError等等。
- 状态决策,由Repo层处理好具体的UIState,然后ViewModel层处理线程的具体调度,UI层拿到相应的UIState进行渲染响应。
kotlin
sealed class MyUIState {
class Success(val data: String): MyUIState()
class Failed(val throwable: Throwable): MyUIState()
object Refreshing: MyUIState()
}
class MyViewModel(
private val repository: Repository,
): ViewModel() {
private val _uiState: MutableLiveData<MyUIState> = MutableLiveData()
val uiState: LiveData<MyUIState> = _uiState
fun fetchData() {
_uiState.value = MyUIState.Refreshing
viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> {
_uiState.value = MyUIState.Failed(it)
}}) {
// Image repository.fetchData() run in background thread to fetch data.
// Warning: when you want to post something to livedata, you need to stay at UI Thread.
_uiState.value = MyUIState.Success(repository.fetchData)
}
}
}
class MyFragment: Fragment() {
private val viewModel: MyViewModel by lazy {
ViewModelProvider(this)[MyViewModel::class.java]
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.uiState.observe(this) {
when (it) {
is MyUIState.Success -> {
showHideSpinner(false)
// updateUI
updateUI(it.data)
}
is MyUIState.Failed -> {
showHideSpinner(false)
// ToastError
Toast.makeText(context, it.throwable.message, Toast.LENGTH_SHORT).show()
}
is MyUIState.Refreshing -> {
showHideSpinner(true)
}
}
}
}
private fun updateUI(data: String) {
// updateUI
println(data)
}
}
📈 图表对比
模式 | 核心组成 | 职责划分 | 数据流动方向 |
---|---|---|---|
MVP (Model–View–Presenter) | Model、View、Presenter | View 负责展示,Presenter 负责业务逻辑,Model 负责数据 | 双向(Presenter 与 View 互相调用) |
MVVM (Model–View–ViewModel) | Model、View、ViewModel | ViewModel 暴露可观察的数据(LiveData、StateFlow...),View 只订阅 | 单向绑定(数据变化自动驱动 UI) |
MVI (Model–View–Intent) | Model、View、Intent(或 State) | 一切交互都是事件,状态是不可变的,View 根据状态渲染 | 单向数据流(更严格的单向) |
🧐 对比分析
特性 | MVP | MVVM | MVI |
---|---|---|---|
数据流方向 | 双向 | 单向(View 观察 ViewModel) | 严格单向 |
状态管理 | 分散在 Presenter 中 | ViewModel 管理可观察状态 | 中心化状态容器(immutable) |
可测试性 | 较好 | 更好(因无 UI 依赖) | 最好(纯函数式状态流) |
复杂度 | 中等 | 中等偏高 | 高(但可维护性好) |
典型应用 | Android 早期(MVP) | Android Jetpack(MVVM) | Jetpack Compose、React、Flutter(MVI) |
补充说明
一般来说,MVVM最好是要分为5层。
- View: 这个是命令式UI下的视图文件以及Activity/Fragment视图容器文件,在这里通过UIState实现数据的刷新。
- ViewModel:这个是用于线程调度、将UIState通知到LiveData(或者Flow),让View能够正常监听到UIState的数据。
- Repository:拼装DataModel成为UIState的一层。
- 这一层是最有意思的一层,它需要点离散数学知识(比如合理的集合映射等等)才能把数据正确拼装起来,其他的层大多都是模板代码
- 其他层的内容,大多都是模版代码,实际上可以考虑使用AOP切面编程 aspectj 将这一块的内容,封装好然后直接调用的。
- UseCase:数据的不同来源,可能是本地数据,也可能是网络数据。
- Service:数据的来源。
当然,如果数据来源只有一个,那么在开发中是可以为了便于开发而舍弃UseCase这一层的,这个要具体问题具体分析。