MVVM 进阶版:MVI 架构了解一下~
前言
Android
开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC
,MVP
,MVVM
等,其中MVVM
更是被官方推荐,成为Android
开发中的显学。
不过软件开发中没有银弹,MVVM
架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI
可以很好的解决一部分MVVM
的痛点。
本文主要包括以下内容
-
MVC
,MVP
,MVVM
等经典架构介绍 -
MVI
架构到底是什么? -
MVI
架构实战
需要重点指出的是,标题中说
MVI
架构是MVVM
的进阶版是指MVI
在MVVM
非常相似,并在其基础上做了一定的改良,并不是说MVI
架构一定比MVVM
适合你的项目
各位同学可以在分析比较各个架构后,选择合适项目场景的架构
经典架构介绍
MVC
架构介绍
MVC
是个古老的Android
开发架构,随着MVP
与MVVM
的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:
MVC
架构主要分为以下几部分
- 视图层(
View
):对应于xml
布局文件和java
代码动态view
部分 - 控制层(
Controller
):主要负责业务逻辑,在android
中由Activity
承担,同时因为XML
视图功能太弱,所以Activity
既要负责视图的显示又要加入控制逻辑,承担的功能过多。 - 模型层(
Model
):主要负责网络请求,数据库处理,I/O
的操作,即页面的数据来源
由于android
中xml
布局的功能性太弱,Activity
实际上负责了View
层与Controller
层两者的工作,所以在android
中mvc
更像是这种形式:
因此MVC
架构在android
平台上的主要存在以下问题:
-
Activity
同时负责View
与Controller
层的工作,违背了单一职责原则 -
Model
层与View
层存在耦合,存在互相依赖,违背了最小知识原则
MVP
架构介绍
由于MVC
架构在Android
平台上的一些缺陷,MVP
也就应运而生了,其架构图如下所示
MVP
架构主要分为以下几个部分
-
View
层:对应于Activity
与XML
,只负责显示UI
,只与Presenter
层交互,与Model
层没有耦合 -
Presenter
层: 主要负责处理业务逻辑,通过接口回调View
层 -
Model
层:主要负责网络请求,数据库处理等操作,这个没有什么变化
我们可以看到,MVP
解决了MVC
的两个问题,即Activity
承担了两层职责与View
层与Model
层耦合的问题
但MVP
架构同样有自己的问题
-
Presenter
层通过接口与View
通信,实际上持有了View
的引用 - 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成
View
的接口会很庞大。
MVVM
架构介绍
MVVM
模式将 Presenter
改名为 ViewModel
,基本上与 MVP
模式完全一致。
唯一的区别是,它采用双向数据绑定(data-binding
):View
的变动,自动反映在 ViewModel
,反之亦然
MVVM
架构图如下所示:
可以看出MVVM
与MVP
的主要区别在于,你不用去主动去刷新UI
了,只要Model
数据变了,会自动反映到UI
上。换句话说,MVVM
更像是自动化的MVP
。
MVVM
的双向数据绑定主要通过DataBinding
实现,不过相信有很多人跟我一样,是不喜欢用DataBinding
的,这样架构就变成了下面这样
-
View
观察ViewModle
的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM
的这一大特性我其实并没有用到 -
View
通过调用ViewModel
提供的方法来与ViewMdoel
交互
小结
-
MVC
架构的主要问题在于Activity
承担了View
与Controller
两层的职责,同时View
层与Model
层存在耦合 -
MVP
引入Presenter
层解决了MVC
架构的两个问题,View
只能与Presenter
层交互,业务逻辑放在Presenter
层 -
MVP
的问题在于随着业务逻辑的增加,View
的接口会很庞大,MVVM
架构通过双向数据绑定可以解决这个问题 -
MVVM
与MVP
的主要区别在于,你不用去主动去刷新UI
了,只要Model
数据变了,会自动反映到UI
上。换句话说,MVVM
更像是自动化的MVP
。 -
MVVM
的双向数据绑定主要通过DataBinding
实现,但有很多人(比如我)不喜欢用DataBinding
,而是View
通过LiveData
等观察ViewModle
的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定
MVI
架构到底是什么?
MVVM
架构有什么不足?
要了解MVI
架构,我们首先来了解下MVVM
架构有什么不足
相信使用MVVM
架构的同学都有如下经验,为了保证数据流的单向流动,LiveData
向外暴露时需要转化成immutable
的,这需要添加不少模板代码并且容易遗忘,如下所示
class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}
如上所示,如果页面逻辑比较复杂,ViewModel
中将会有许多全局变量的LiveData
,并且每个LiveData
都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM
架构写比较复杂页面时最难受的点。
其次就是View
层通过调用ViewModel
层的方法来交互的,View
层与ViewModel
的交互比较分散,不成体系
小结一下,在我的使用中,MVVM
架构主要有以下不足
- 为保证对外暴露的
LiveData
是不可变的,需要添加不少模板代码并且容易遗忘 -
View
层与ViewModel
层的交互比较分散零乱,不成体系
MVI
架构是什么?
MVI
与 MVVM
很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示
其主要分为以下几部分
-
Model
: 与MVVM
中的Model
不同的是,MVI
的Model
主要指UI
状态(State
)。例如页面加载状态、控件位置等都是一种UI
状态 -
View
: 与其他MVX
中的View
一致,可能是一个Activity
或者任意UI
承载单元。MVI
中的View
通过订阅Model
的变化实现界面刷新 -
Intent
: 此Intent
不是Activity
的Intent
,用户的任何操作都被包装成Intent
后发送给Model
层进行数据请求
单向数据流
MVI
强调数据的单向流动,主要分为以下几步:
- 用户操作以
Intent
的形式通知Model
-
Model
基于Intent
更新State
-
View
接收到State
变化刷新UI。
数据永远在一个环形结构中单向流动,不能反向流动:
上面简单的介绍了下MVI
架构,下面我们一起来看下具体是怎么使用MVI
架构的
MVI
架构实战
总体架构图
我们使用ViewModel
来承载MVI
的Model
层,总体结构也与MVVM
类似,主要区别在于Model
与View
层交互的部分
-
Model
层承载UI
状态,并暴露出ViewState
供View
订阅,ViewState
是个data class
,包含所有页面状态 -
View
层通过Action
更新ViewState
,替代MVVM
通过调用ViewModel
方法交互的方式
MVI
实例介绍
添加ViewState
与ViewEvent
ViewState
承载页面的所有状态,ViewEvent
则是一次性事件,如Toast
等,如下所示
data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)
sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}
- 我们这里
ViewState
只定义了两个,一个是请求状态,一个是页面数据 -
ViewEvent
也很简单,一个简单的密封类,显示Toast
与Snackbar
ViewState
更新
class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()
init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}
private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}
private fun emit(state: MainViewState?) {
_viewStates.value = state
}
private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}
如上所示
- 我们只需定义
ViewState
与ViewEvent
两个State
,后续增加状态时在data class
中添加即可,不需要再写模板代码 -
ViewEvents
是一次性的,通过SingleLiveEvent
实现,当然你也可以用Channel
当来实现 - 当状态更新时,通过
emit
来更新状态
View
监听ViewState
private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}
如上所示,MVI
使用 ViewState
对 State
集中管理,只需要订阅一个 ViewState
便可获取页面的所有状态,相对 MVVM
减少了不少模板代码。
View
通过Action
更新State
class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)
private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}
如上所示,View
通过Action
与ViewModel
交互,通过 Action
通信,有利于 View
与 ViewModel
之间的进一步解耦,同时所有调用以 Action
的形式汇总到一处,也有利于对行为的集中分析和监控
总结
本文主要介绍了MVC
,MVP
,MVVM
与MVI
架构,目前MVVM
是官方推荐的架构,但仍然有以下几个痛点
-
MVVM
与MVP
的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg
,其实并没有使用MVVM
双向绑定的特性,而是单一数据源 - 当页面复杂时,需要定义很多
State
,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘 -
View
与ViewModel
通过ViewModel
暴露的方法交互,比较零乱难以维护
而MVI
可以比较好的解决以上痛点,它主要有以下优势
- 强调数据单向流动,很容易对状态变化进行跟踪和回溯
- 使用
ViewState
对State
集中管理,只需要订阅一个ViewState
便可获取页面的所有状态,相对MVVM
减少了不少模板代码 -
ViewModel
通过ViewState
与Action
通信,通过浏览ViewState
和Aciton
定义就可以理清ViewModel
的职责,可以直接拿来作为接口文档使用。
当然MVI
也有一些缺点,比如
- 所有的操作最终都会转换成
State
,所以当复杂页面的State
容易膨胀 -
state
是不变的,因此每当state
需要更新时都要创建新对象替代老对象,这会带来一定内存开销
软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。
但通过以上的分析与介绍,我相信使用MVI
架构代替没有使用DataBinding
的MVVM
是一个比较好的选择~
更多
关于MVI
架构更佳实践,支持局部刷新,可参见: MVI 架构更佳实践:支持 LiveData 属性监听
关于MVI
架构封装,优雅实现网络请求,可参见: MVI 架构封装:快速优雅地实现网络请求
项目地址
本文所有代码可见:github.com/shenzhen201…
参考资料
基于Android的MVI架构:从双向绑定到单向数据流
Jetpack Compose 架构如何选? MVP, MVVM, MVI
站在思想层面看MVX架构
Best Architecture For Android : MVI + LiveData + ViewModel = ❤️
作者:程序员江同学
链接:https://juejin.cn/post/7022624191723601928