Android MVI 架构学习
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MVI 架构学习相关的知识,希望对你有一定的参考价值。
文章目录
1. 概述
1.1 android 架构的背景
这些年来,Android 上发展了多种主流架构,从最开始的 MVC
,到 Clean
和 MVP
,再到现在最火热的 MVVM
,可以说架构发展一直很卷,这不,MVVM
还没有用个几年呢, MVI
就出来了。
要说 Android 架构卷,实则不然,上面说的这些架构其实根本不是来自 Android 的,而是源自于 Web,即大前端, Web 由于其自身特性(还不算完全成熟,业务多且杂,热部署等),版本迭代速度巨快,技术的更新迭代也因此很快,上面这些架构最早就是在前端所应用和发展出来的, 而移动端则是直接抄来,跟着Web的步伐前进。
所以 MVI
显然不是 Android 架构的终点,说不定明年 Web 上就又弄出一个新玩意把它取代了。不学习 MVI
并不会让我们落伍,现阶段 MVP
、MVVM
足以解决Android所有的业务场景。
但是学习 MVI
有这么几个好处:
- 装杯:
新鲜的架构会让代码逼格提升,展示代码时,你可以用 “唯一可信刷新点”、“数据单向流动” 等词语来装杯,而且 MVI 会使用协程Flow
,Flow 可是 Google 这两年很推荐的框架呢 - 官方认定:
Android 官网在去年就已经有推荐使用MVI
的文章了, 在 Google 开发者大会中也有专门直播介绍 MVI 架构,所以它是官方认可的架构,至少不会那么容易被淘汰,而且面试也有可能会问到 - 素材好找,容易引用:
MVI
并不会引入什么三方库。比起具体的架构形态,它的形态更像是一种设计思想,基于已有的MVP
、MVVM
进行调整,也能搞出MVI
,所以它其实离我们很近,方便我们直接拿代码出来撸
除此之外就真真真真没了,各种文章都会踩一捧一说 MVI
的多个优点(虽然本文也会一样),但是架构终究是为项目服务,如果你的项目能够快速开发出一个 MVP
的界面,你就可以花更多时间在 Debug、单元测试等提升质量的事情上, 而如果你的项目比较慢才能搞出一个 MVVM
/ MVI
页面,那因为时间问题,你很有可能就少测几个用例,多埋了几个雷。
So , MVI
目前并不在 Android 的必修课中,你不会也不用烦恼,请抱着休闲的心态来学习吧~
1.2 MVC
MVC 是最早的明确把 Android 页面框架划分为 视图层(View)、逻辑层(Controller)和 数据模型层(Model) 的架构,它们的关系如下图所示:
逻辑单元的流动过程是:
- View层 可以调用 Controller层,触发一些逻辑操作,例如点击视图某个按钮,可以调用网络请求,也可以直接修改 Model层数据
- Controller层 操作后,可以直接操作 Model层,例如对数据库、内存的修改
- View层 直接持有 Model层,所以在感知其变化后,会触发 UI 更新,以将最新的数据展示在屏幕上
MVC 的缺点有:
- Controller层 往往都已
Activity
/Fragment
为载体,它们正好又是视图UI,所以页面逻辑稍微复杂一点,就会导致Activity
的代码臃肿膨胀,不好维护,代码可读性差 - View层 和 Model层 直接耦合,虽然看起来很方便,但是这会出现一个重要问题:可复用性、扩展能力差
①:对于 Model 来说:假设网络接口调整,一个数据 Bean 的结构需要修改,它除了自身代码改变,它还会直接影响到 View 层的代码,触一发而动全身,扩展性很差
②:对于 View 来说:因为和 Model 进行直接绑定,所以不好在其它数据源不同的地方复用 - Model层 会被多个地方修改,出现问题时,得从 View层 和 Controller层 进行 Debug
1.3 MVP
MVP 的好处就是把 MVC 的缺点解决了, View层 和 Model层 不直接耦合,将逻辑层改了个名字,叫 Presenter
,如下图所示:
逻辑单元的流动过程是:
- View 直接持有 Presenter,可以通知 Presenter 进行逻辑操作
- Presenter 可以通过网络请求等数据处理逻辑,操作 Model
- Model 通过回调或其它方式通知 Presenter
- Presenter 通过接口或直接持有 View 来通知 View 进行 UI更新
MVP 的缺点是:
- 因为 Presenter 会以接口的方式来通知 View 更新 UI,所以复杂业务容易导致接口函数爆炸多,不符合接口的方法应该尽可能少的原则
- Presenter 无法感知 View 的生命周期,比如在 View 销毁后,可能 Presenter 还在做一些耗时操作,导致出现性能问题
1.4 MVVM(无 DataBinding 版)
Jetpack 出来后,它通过 LifeCycle
为 Presenter 赋予了能感知 View 生命周期的能力,并改了个名字叫 ViewModel
。
Jetpack 甚至直接提供了 ViewModel
类、 LiveData
类等来让我们使用,非常方便。
class MyViewModel : ViewModel()
override fun onCleared()
// 释放资源
在没有使用 DataBinding
时, MVVM 和 MVP 其实是差不多的,如下图所示:
无 DataBinding 版的缺点是:
- MVVM 的初衷是数据双向绑定, 无
DataBinding
版的 MVVM 没有做到这点 - View 会同时订阅多个
LiveData
, 每个LiveData
都可以看做是触发 View 更新的一个刷新点,假设 UI 展示出现异常,我们需要从众多刷新点中找到有问题的那一个,调试上可能会比较麻烦~
1.5 MVVM(DataBinding 版)
MVVM的核心是双向绑定,也就是 View 和 ViewModel 的一种自动绑定,这种能力是:
View
可以直接触达ViewModel
逻辑,而无需通过在代码中写setClickListener viewModel.onClick()
等逻辑ViewModel
的变化可以直接更新View
, 而无需通过在View
代码中写setText=xxx
、setXXX
等逻辑
总结就是加了 DataBinding
/ ViewBinding
后 ,好处就是 Activity
/ Fragment
可以少写一些诸如 findViewById
、setXXX
等的样板代码:
MVVM 有 DataBinding 版的缺点是:
- 双向绑定对调试不太友好,UI 出现异常时,需要定位问题是出现在 V 层还是出现在 M 层,虽然 MVP 也是这样,但是用了
DataBinding
的代码,往往都很不直观,跳来跳去的看得费神 DataBinding
会直接注入到 View 的 XML 文件中,这使得这个 View 不好在其它地方被直接复用
1.6 MVI 的起源
MVI 模式来源于2014年的 Cycle.js
(一个 javascript框架),并且在主流的 JS 框架 Redux
中大行其道,然后就被一些大佬移植到了 Android 上(比如最早期用Java写的 mosby)。
在 MVI(Model-View-Intent) Pattern in Android这篇文章上说明了搞出 MVI 是为了解决什么问题的:
对于主要的GUI体系结构,MVI的定义相当松散,它的核心是回归MVC提供的单向数据流…… 尽管还有一些其他的部分
单看这句话,MVI 就像是 MVC 的一种衍生产物, 我们知道 MVP 也是 MVC 的衍生产物,MVVM 也是 MVP 的一种衍生,如下图所示(因为 MVI 是一种松散的定义,而 MVP、MVVM是一种对框架的强制定义,所以我对 MVI 使用了虚线):
这样看来,MVI 好像不是根据最先进的 MVVM 发展而来的,而是一种新分支,它的出现不是为了解决 MVP、MVVM 所存在的问题,而是基于MVC提供的M和V层来实现一些能力。
下面,我们将来探究 MVI 具备样什么特性。
2. MVI 特性
MVI 的核心是:唯一可信数据源的单向流动。
2.1 数据的单向流动
数据的单向流动,其实就是不想让数据双向流动。
数据的双向流动就是使用 DataBinding
那样:数据模型的变化会同步到视图上,视图上的操作同时也会同步到数据模型上。
而数据的单向流动,强调的是数据的源头只有一个,目的地只有一个,数据的流向是易追踪的。
这两者其实可以下面这种简单的方式来区分:
- 如果需要手动让
View
观察ViewModel
(视图数据)来更新自身,那么数据就是单向流动的 - 如果不需要手动让
View
观察,ViewModel
的更新能够自动触发View
的更新,那么数据就是双向绑定的
那其实 MVC、MVP、MVVM(无 DataBinding
) 都是数据单向流动的框架。
2.2 唯一可信数据源
MVI 的愿景是能让 View 触发刷新的状态只有一个。
举个例子,假设一个 View 上有多个 UI 控件,用户不同的操作可以触发不同的 UI 控件刷新:
// Activity / Fragment 的代码
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
vm = viewModel()
// 是否弹出/关闭 Loading 的动画
vm.showLoading.observe(this, Observer
showOrCloseLoading(it)
)
// button 是否要置灰
vm.buttonState.observe(this, Observer
setButtonState(it)
)
// textView 更新UI
vm.titleText.observe(this, Observer
textView.text = it.toString()
)
... 设置一些监听用户操作的事件
上面代码中有三个可以影响 UI 刷新的地方,也就是说有三个更新点。
而 MVI 中只能有一个更新点, 上面的代码要做到只有一个更新点,那就相当于要收归所有更新的地方,变成下面这样:
sealed class UiState
class LoadingState(showLoading: Boolean): UiState()
class ButtonState(color: Color): UiState()
class TitleState(title: String): UiState()
...
vm.uiState.observe(this, Observer state ->
when (state)
is LoadingState -> showOrCloseLoading(state.value),
is ButtonState -> setButtonState(state.color)
is TitleState -> textView.text = state.title
)
虽然第一眼看上去没有什么卵用,但上面代码确实做到了唯一可信数据源,uiState
是数据源,而 UI 刷新只依赖这个数据源,就让它具备了唯一可信的属性。(因为不会在有别的状态会触发 UI 更新了!)
2.3 MVI 各层
MVI 将架构分成了三个部分:
I - Intent
意图:它是简单描述用户与App交互时产生的一个动作或命令。例如按钮的点击,页面的滑动切换V - View
视图:实际的 UI 组件M - ViewModel + Model
视图模型 + 数据层:该层就是数据层,它一大一小有两层:ViewModel
: 例如需要展示在TextView
上的文案、ImageView
上的图片资源等Repository / DataStore
: 例如需要通过数据库或网络请求得到的数据,它一般作为ViewModel
的数据源
它们的关系如下所示:
对于 V 层和 M层 都是比较好理解的,而 I层 和我们之前所遇到的 Controller
、 Presenter
有所不同,它不是一个单独具体的实体,而是一个描述数据流动的模型。
那么它要如何表示呢?
2.4 Intent和响应式编程
MVI 认为只要视图还存在,用户就会源源不断地和视图界面进行交互,所以在 UI 的生命周期内会产生很多用户操作所产生出的数据。
这些源源不断数据则可以用数据流来表示,数据流模型可以简单的描述为在生产者-消费者模型下,生产者作为上游可以不断产生数据,下游的消费者接收这些数据并进行处理消费,而用户的交互和程序的响应正好能对应这个模型:
- 生产者 —— 数据流的起点 —— 用户的交互,例如点击某个按钮, 它能产生一个信号,来让 App 去请求网络数据
- 消费者 —— 数据流的终点 —— UI, 将网络请求回来的数据进行层层处理,然后显示在屏幕上
而 Intent 就是生产者, 即数据流的起点,例如下面代码就是用户由交互产生的一个意图:
button.setOnClickListener
// 产生一个意图
viewModel.sendIntent(BUTTON_CLICK)
其次,处理数据流的模型是响应式编程模型, 我们知道一些专门的框架,例如 RxJava
、Flow
,这里不再具体介绍了…
所以这里的意图作为数据流的起点,也应该使用响应式编程的做法,所以我们一般看到的示例代码都是用 协程的 Channel
和 StateFlow
举例子的(这里不了解的同学可以看这篇文章:深潜Kotlin协程(十六):Channel 和 深潜Kotlin协程(二十三 完结篇):SharedFlow 和 StateFlow):
button.setOnClickListener
lifecycleScope.launch
// 产生一个意图, ViewModel 使用一个 Channel 来接收意图
viewModel.channel.send(BUTTON_CLICK)
具体的代码可以放到第三节再看。
最后,MVI 框架已经初具雏形,它是一个 单向数据流+唯一可信数据源+响应式编程的模型,和 MVP、MVVM 相比,它主要的区别是引入了数据流这一概念,因为 Kotlin 的协程和 Jetpack 的支持,我们现在可以很舒服的在 Android 框架中使用响应式编程,所以这也是 MVI 为什么在 Android 框架上开始流行的原因。
它的数据流动可以用下面这张图来概括:
3. 示例
我们可以基于 MVP 或者无 DataBinding
版本的 MVVM 搞出 MVI模式。 由于我们需要使用响应式编程,而 ViewModel 提供了协程作用域,方便于我们使用 Flow
,所以 MVVM 相较于 MVP 能够更舒服的做出 MVI。所以基本上所有的 MVI 架构都是用 MVVM 来写的,即使用 ViewModel 而非 Presenter。
3.1 定义和处理 Intent
意图描述用户交互,所以我们可以把所有用户有关的操作都写出来,并用 场景名+Intent
来命名,假设我们的界面是一个新闻列表页面:
// 新闻界面所有的用户操作, 基本类
sealed class NewsIntent
class UserClickNewsIntent(val url: String): NewsIntent() // 用户点击具体某个新闻Item的交互
object RefreshNewsIntent : NewsIntent() // 用户刷新新闻列表的交互
并且在 VieawModel 中定义数据流的开端:
class NewsViewModel : ViewModel()
// 定义接收意图的通道
val newsIntent = Channel<NewsIntent>()
init
handleIntent()
private fun handleIntent()
viewModelScope.launch
newsIntent.consumeAsFlow().collect
when (it)
is UserClickNewsIntent -> intoNewsItem(it.url) // 处理 UserClickNewsIntent 意图
is RefreshNewsIntent -> fetchNews() // 处理 RefreshNewsIntent 意图
private suspend fun intoNewsItem(url: String)
...
private suspend fun fetchNews()
...
3.2 触发 Intent
在 Activity / Fragment 这种第一层级的视图中,定义触发 Intent
的逻辑,一般是通过点击事件等操作,和 MVVM 中的触发逻辑一样,不过这里要在协程中触发,并且使用 Channel
或其它 Flow 工具,因为这样做是一种响应式编程的逻辑。
class NewsActivity : ComponentActivity()
private val viewModel: NewsViewModel = ViewModelProvider(this).get(NewsViewModel::class.java)
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?)
super.onCreate(savedInstanceState, persistentState)
initListener()
private fun initListener()
refreshButton.setOnClickListener
sendIntent(RefreshNewsIntent)
createNewsItem(onItemClick = newsItem ->
sendIntent(UserClickNewsIntent(newsItem.url))
)
private fun sendIntent(intent: NewsIntent)
lifecycleScope.launch
viewModel.newsIntent.send(intent)
3.3 定义 UiState 作为 View 的唯一数据源
我们通过归纳新闻页的页面状态,可以分成 初始态、加载中、加载成功、加载失败 四个状态,那么我们将状态收归到 UiState 中去,使用 功能名+UiState
来命名:
sealed class NewsUiState // 新闻页面的 UI 状态
object NewsUiStateInitial: NewsUiState() // 初始状态
object Loading: NewsUiState() // 正在加载
class LoadingSuccess(val newsList: List<NewsItem>): NewsUiState() // 加载成功
class LoadingFail(val errorMessage: String): NewsUiState() // 加载失败
ViewModel 持有 NewsUiState
,并暴露出去,类似于 LiveData
那样子 :
class NewsViewModel : ViewModel()
private val _newsUiState = MutableStateFlow<NewsUiState>(NewsUiStateInitial)
val newsUiState: StateFlow<NewsUiState> = _newsUiState
Activity 依赖 ViewModel 持有的 UiState, 用于进行视图刷新:
class NewsActivity : ComponentActivity()
...
private fun observerUiState()
lifecycleScope.launch
// 这里使用 repeatOnLifecycle 包装一下性能更好
viewModel.newsUiState.collect
when(it)
is NewsUiStateInitial -> initial()
is Loading -> loading()
is LoadingSuccess -> updateNewsList(it.newsList)
is LoadingFail -> showError(it.errorMessage)
3.4 刷新 UiState
ViewModel 在处理完成后,通过更新 newsUiState
,来触发 View 的刷新:
class NewsViewModel(private val dataStore: NewsDataStore = NewsDataStore()) : ViewModel()
private suspend fun fetchNews()
dataStore.fetchNews.flowOn(Dispatchers.Default)
.catch
_newsUiState.value = LoadingFail("加载失败啦")
.collect
if (it.isEmpty()) _newsUiState.value = LoadingSuccess(it)
else _newsUiState.value = LoadingFail("数据是空的")
..
data class NewsData(private val title: String)
class NewsDataStore()
// 用于获取新闻列表的 Flow
val fetchNews: Flow<List<NewsData>> = flow
val news = fetchData()
emit(news)
suspend fun fetchData(): List<NewsData> = api.getNewsData()
4. 总结
无论是 MVC、MVP、MVVM 还是 MVI,它们的共同点都是有 M层 和 V层。
所以这些架构的区别,也就是 MV 后面那个字母的区别,但万变不离其中的是:它们的作用是描述 Model 和 View 之间的关系。
- MVI 是基于 MVC 的发展,它是核心是数据的单向流动,优点是易追踪问题
- MVI 引入了数据流模型,所以使用了 响应式编程模型来处理数据流,而
Intent
意图,就是这个数据流的起点,即生产者,而View
则是数据流的重点,即消费者 - MVI 中,整个 View 只依赖一个
State
刷新,这个State
就是唯一可信数据源, 仅依赖单一状态的 UI 是更好测试的 - 在 Kotlin Anroid 中,因为 MVVM 很好的支持协程
Flow
(响应式模型),所以我们一般用 MVVM 来写 MVI
综上所述,MVI 和 MVP、MVVM 这两位并不是一个维度的东西。MVI 强调数据的流动方向,而后两者则是强调结构分层。
MVI 的缺点是:
- UiState 的代码量容易随着视图的复杂度增加而增加
- UiState 爆炸时,UI 的更新点可能就会变多,频繁触发更新会导致内存消耗多, 这里需要实现一个状态的局部刷新,把更新频率尽可能降低
但是这些缺点, 大前端早就已经克服了,Android 肯定也有对应的实现,这里就不再赘述,后面遇到了再记录。
参考
MVI(Model-View-Intent) Pattern in Android
官网
以上是关于Android MVI 架构学习的主要内容,如果未能解决你的问题,请参考以下文章