MVVM|MVVM 成为历史,Google 全面倒向 MVI
前言
前段时间写了一些介绍MVI
架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时众所周知,Google
推荐的是MVVM
架构。相信很多人都会有疑问,我为什么不使用官方推荐的MVVM
,而要用你说的这个什么MVI
架构呢?
不过我这几天查看Android
的应用架构指南,发现谷歌推荐的最佳实践已经变成了单向数据流动
+ 状态集中管理
,这不就是MVI
架构吗?看起来Google
已经开始推荐使用MVI
架构了,大家也有必要开始了解一下Android
应用架构指南的最新版本了~
总体架构
两个架构原则
Android
的架构设计原则主要有两个
分离关注点
要遵循的最重要的原则是分离关注点。一种常见的错误是在一个 Activity
或 Fragment
中编写所有代码。这些基于界面的类应仅包含处理界面和操作系统交互的逻辑。总得来说,Activity
或Fragment
中的代码应该尽量精简,尽量将业务逻辑迁移到其它层
通过数据驱动界面
另一个重要原则是您应该通过数据驱动界面(最好是持久性模型)。数据模型独立于应用中的界面元素和其他组件。
这意味着它们与界面和应用组件的生命周期没有关联,但仍会在操作系统决定从内存中移除应用的进程时被销毁。
数据模型与界面元素,生命周期解耦,因此方便复用,同时便于测试,更加稳定可靠。
推荐的应用架构
基于上一部分提到的常见架构原则,每个应用应至少有两个层:
- 界面层 - 在屏幕上显示应用数据。
- 数据层 - 提供所需要的应用数据。
文章图片
如上所示,各层之间的依赖关系是单向依赖的,网域层,数据层不依赖于界面层
界面层 界面的作用是在屏幕上显示应用数据,并响应用户的点击。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为外部输入(例如网络响应),界面都应随之更新,以反映这些变化。
不过,从数据层获取的应用数据的格式通常不同于
UI
需要展示的数据的格式,因此我们需要将数据层数据转化为页面的状态 因此界面层一般分为两部分,即
UI
层与State Holder
,State Holder
的角色一般由ViewModel
承担文章图片
数据层的作用是存储和管理应用数据,以及提供对应用数据的访问权限,因此界面层必须执行以下步骤:
- 获取应用数据,并将其转换为
UI
可以轻松呈现的UI State
。 - 订阅
UI State
,当页面状态发生改变时刷新UI
- 接收用户的输入事件,并根据相应的事件进行处理,从而刷新
UI State
- 根据需要重复第 1-3 步。
文章图片
因此界面层主要需要做以下工作:
- 如何定义
UI State
。 - 如何使用单向数据流 (
UDF
),作为提供和管理UI State
的方式。 - 如何暴露与更新
UI State
- 如何订阅
UI State
UI State
如果我们要实现一个新闻列表界面,我们该怎么定义
UI State
呢?我们将界面需要的所有状态都封装在一个data class
中。 与之前的
MVVM
模式的主要区别之一也在这里,即之前通常是一个State
对应一个LiveData
,而MVI
架构则强调对UI State
的集中管理data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List = listOf(),
val userMessages: List = listOf()
)data class NewsItemUiState(
val title: String,
val body: String,
val bookmarked: Boolean = false,
...
)
以上示例中的
UI State
定义是不可变的。这样的主要好处是,不可变对象可保证即时提供应用的状态。这样一来,UI
便可专注于发挥单一作用:读取UI State
并相应地更新其UI
元素。因此,切勿直接在UI
中修改UI State
。违反这个原则会导致同一条信息有多个可信来源,从而导致数据不一致的问题。例如,如上中来自
UI State
的NewsItemUiState
对象中的bookmarked
标记在Activity
类中已更新,那么该标记会与数据层展开竞争,从而产生多数据源的问题。UI State
集中管理的优缺点
在MVVM
中我们通常是多个数据流,即一个State
对应一个LiveData
,而MVI
中则是单个数据流。两者各有什么优缺点? 单个数据流的优点主要在于方便,减少模板代码,添加一个状态只需要给
data class
添加一个属性即可,可以有效地降低ViewModel
与View
的通信成本 同时
UI State
集中管理可以轻松地实现类似MediatorLiveData
的效果,比如可能只有在用户已登录并且是付费新闻服务订阅者时,您才需要显示书签按钮。您可以按如下方式定义UI State
:data class NewsUiState(
val isSignedIn: Boolean = false,
val isPremium: Boolean = false,
val newsItems: List = listOf()
){
val canBookmarkNews: Boolean get() = isSignedIn && isPremium
}
如上所示,书签的可见性是其它两个属性的派生属性,其它两个属性发生变化时,
canBookmarkNews
也会自动变化,当我们需要实现书签的可见与隐藏逻辑,只需要订阅canBookmarkNews
即可,这样可以轻松实现类似MediatorLiveData
的效果,但是远比MediatorLiveData
要简单当然,
UI State
集中管理也会有一些问题:- 不相关的数据类型:
UI
所需的某些状态可能是完全相互独立的。在此类情况下,将这些不同的状态捆绑在一起的代价可能会超过其优势,尤其是当其中某个状态的更新频率高于其他状态的更新频率时。 UiState diffing
:UiState
对象中的字段越多,数据流就越有可能因为其中一个字段被更新而发出。由于视图没有diffing
机制来了解连续发出的数据流是否相同,因此每次发出都会导致视图更新。当然,我们可以对LiveData
或Flow
使用distinctUntilChanged()
等方法来实现局部刷新,从而解决这个问题
UI State
上文提到,为了保证
UI
中不能修改状态,UI State
中的元素都是不可变的,那么如何更新UI State
呢? 我们一般使用
ViewModel
作为UI State
的容器,因此响应用户输入更新UI State
主要分为以下几步:ViewModel
会存储并公开UI State
。UI State
是经过ViewModel
转换的应用数据。UI
层会向ViewModel
发送用户事件通知。ViewModel
会处理用户操作并更新UI State
。- 更新后的状态将反馈给
UI
以进行呈现。 - 系统会对导致状态更改的所有事件重复上述操作。
ViewModel
,然后ViewModel
更新UI State
(中间可能有数据层的更新),UI
层订阅UI State
订响应刷新,从而完成页面刷新,如下图所示:文章图片
为什么使用单向数据流动? 单向数据流动可以实现关注点分离原则,它可以将状态变化来源位置、转换位置以及最终使用位置进行分离。
这种分离可让
UI
只发挥其名称所表明的作用:通过观察UI State
变化来显示页面信息,并将用户输入传递给ViewModel
以实现状态刷新。换句话说,单向数据流动有助于实现以下几点:
- 数据一致性。界面只有一个可信来源。
- 可测试性。状态来源是独立的,因此可独立于界面进行测试。
- 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。
UI State
定义好UI State
并确定如何管理相应状态后,下一步是将提供的状态发送给界面。我们可以使用LiveData
或者StateFlow
将UI State
转化为数据流并暴露给UI
层 为了保证不能在
UI
中修改状态,我们应该定义一个可变的StateFlow
与一个不可变的StateFlow
,如下所示:class NewsViewModel(...) : ViewModel() {private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow = _uiState.asStateFlow()...}
这样一来,
UI
层可以订阅状态,而ViewModel
也可以修改状态,以需要执行异步操作的情况为例,可以使用viewModelScope
启动协程,并且可以在操作完成时更新状态。class NewsViewModel(
private val repository: NewsRepository,
...
) : ViewModel() {private val _uiState = MutableStateFlow(NewsUiState())
val uiState: StateFlow = _uiState.asStateFlow()private var fetchJob: Job? = nullfun fetchArticles(category: String) {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
try {
val newsItems = repository.newsItemsForCategory(category)
_uiState.update {
it.copy(newsItems = newsItems)
}
} catch (ioe: IOException) {
// Handle the error and notify the notify the UI when appropriate.
_uiState.update {
val messages = getMessagesFromThrowable(ioe)
it.copy(userMessages = messages)
}
}
}
}
}
在上面的示例中,
NewsViewModel
类会尝试进行网络请求,然后更新UI State
,然后UI
层可以对其做出适当反应订阅
UI State
订阅
UI State
很简单,只需要在UI
层观察并刷新UI
即可class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
UI State
实现局部刷新
因为MVI
架构下实现了UI State
的集中管理,因此更新一个属性就会导致UI State
的更新,那么在这种情况下怎么实现局部刷新呢? 我们可以利用
distinctUntilChanged
实现,distinctUntilChanged
只有在值发生变化了之后才会回调刷新,相当于对属性做了一个防抖,因此我们可以实现局部刷新,使用方式如下所示class NewsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Bind the visibility of the progressBar to the state
// of isFetchingArticles.
viewModel.uiState
.map { it.isFetchingArticles }
.distinctUntilChanged()
.collect { progressBar.isVisible = it }
}
}
}
}
当然我们也可以对其进行一定的封装,给
Flow
或者LiveData
添加一个扩展函数,令其支持监听属性即可,使用方式如下所示class MainActivity : AppCompatActivity() {
private fun initViewModel() {
viewModel.viewStates.run {
//监听newsList
observeState(this@MainActivity, MainViewState::newsList) {
newsRvAdapter.submitList(it)
}
//监听网络状态
observeState(this@MainActivity, MainViewState::fetchStatus) {
//..
}
}
}
}
关于
MVI
架构下支持属性监听,更加详细地内容可见:MVI 架构更佳实践:支持 LiveData 属性监听网域层 网域层是位于界面层和数据层之间的可选层。
文章图片
网域层负责封装复杂的业务逻辑,或者由多个
ViewModel
重复使用的简单业务逻辑。此层是可选的,因为并非所有应用都有这类需求。因此,您应仅在需要时使用该层。 网域层具有以下优势:
- 避免代码重复。
- 改善使用网域层类的类的可读性。
- 改善应用的可测试性。
- 让您能够划分好职责,从而避免出现大型类。
APP
,网域层似乎并没有必要,对于ViewModel
重复的逻辑,使用util
来说一般就已足够 或许网域层适用于特别大型的项目吧,各位可根据自己的需求选用,关于网域层的详细信息可见:https://developer.android.com...
数据层 数据层主要负责获取与处理数据的逻辑,数据层由多个
Repository
组成,其中每个Repository
可包含零到多个Data Source
。您应该为应用处理的每种不同类型的数据创建一个Repository
类。例如,您可以为与电影相关的数据创建 MoviesRepository
类,或者为与付款相关的数据创建 PaymentsRepository
类。当然为了方便,针对只有一个数据源的Repository
,也可以将数据源的代码也写在Repository
,后续有多个数据源时再做拆分文章图片
数据层跟之前的
MVVM
架构下的数据层并没用什么区别,这里就不多介绍了,关于数据层的详细信息可见:https://developer.android.com...总结 相比老版的架构指南,新版主要是增加了网域层并修改了界面层,其中网域层是可选的,各位各根据自己的项目需求使用。
而界面层则从
MVVM
架构变成了MVI
架构,强调了数据的单向数据流动
与状态的集中管理
。相比MVVM
架构,MVI
架构主要有以下优点- 强调数据单向流动,很容易对状态变化进行跟踪和回溯,在数据一致性,可测试性,可维护性上都有一定优势
- 强调对
UI State
的集中管理,只需要订阅一个ViewState
便可获取页面的所有状态,相对MVVM
减少了不少模板代码 - 添加状态只需要添加一个属性,降低了
ViewModel
与View
层的通信成本,将业务逻辑集中在ViewModel
中,View
层只需要订阅状态然后刷新即可
Google
在指南中推荐使用MVI
而不再是MVVM
,很可能是为了统一Android
与Compose
的架构。因为在Compose
中并没有双向数据绑定,只有单向数据流动,因此MVI
是最适合Compose
的架构。当然如果你的项目中没有使用
DataBinding
,或许也可以开始尝试一下使用MVI
,不使用DataBinding
的MVVM
架构切换为MVI
成本不高,切换起来也比较简单,在易用性,数据一致性,可测试性,可维护性等方面都有一定优势,后续也可以无缝切换到Compose
。如果看完本文对你有收获,请动动你发财的小手,点点赞,你的点赞是我最大的动力。
相关学习视频推荐: 【2021最新版】Android studio安装教程+Android(安卓)零基础教程视频(适合Android 0基础,Android初学入门)含音视频_哔哩哔哩_bilibili
Android架构设计原理与实战——Jetpack结合MVP组合应用开发一个优秀的APP!_哔哩哔哩_bilibili
Android进阶必学:jetpack架构组件—Navigation_哔哩哔哩_bilibili
【MVVM|MVVM 成为历史,Google 全面倒向 MVI】Android进阶系统学习——Jetpack先天优秀的基因可以避免数据内存泄漏_哔哩哔哩_bilibili
推荐阅读
- 历史教学书籍
- 五年后,我要成为独立自强自信的女性
- 让眼泪滑落,成为骄傲(三十九)
- 论刘备的成功之道
- 不让记忆、感觉、情绪成为孩子的负累|不让记忆、感觉、情绪成为孩子的负累|《全脑教养法》(四)
- 我的朋友,你一定会成为富妈妈
- 写作吧,它会成为一剂良药
- 四个方法帮你成为高效学习者
- 具备这几点你就能成为最会学习的人
- 【0522~今日悦读】如何成为一个幸福的人