这些年来,Android 上发展了多种主流架构,从最开始的 MVC
,到 Clean
和 MVP
,再到现在最火热的 MVVM
,可以说架构发展一直很卷,这不,MVVM
还没有用个几年呢, MVI
就出来了。
要说 Android 架构卷,实则不然,上面说的这些架构其实根本不是来自 Android 的,而是源自于 Web,即大前端, Web 由于其自身特性(还不算完全成熟,业务多且杂,热部署等),版本迭代速度巨快,技术的更新迭代也因此很快,上面这些架构最早就是在前端所应用和发展出来的, 而移动端则是直接抄来,跟着Web的步伐前进。
所以 MVI
显然不是 Android 架构的终点,说不定明年 Web 上就又弄出一个新玩意把它取代了。不学习 MVI
并不会让我们落伍,现阶段 MVP
、MVVM
足以解决Android所有的业务场景。
但是学习 MVI
有这么几个好处:
Flow
,Flow 可是 Google 这两年很推荐的框架呢MVI
的文章了, 在 Google 开发者大会中也有专门直播介绍 MVI 架构,所以它是官方认可的架构,至少不会那么容易被淘汰,而且面试也有可能会问到MVI
并不会引入什么三方库。比起具体的架构形态,它的形态更像是一种设计思想,基于已有的 MVP
、MVVM
进行调整,也能搞出 MVI
,所以它其实离我们很近,方便我们直接拿代码出来撸除此之外就真真真真没了,各种文章都会踩一捧一说 MVI
的多个优点(虽然本文也会一样),但是架构终究是为项目服务,如果你的项目能够快速开发出一个 MVP
的界面,你就可以花更多时间在 Debug、单元测试等提升质量的事情上, 而如果你的项目比较慢才能搞出一个 MVVM
/ MVI
页面,那因为时间问题,你很有可能就少测几个用例,多埋了几个雷。
So , MVI
目前并不在 Android 的必修课中,你不会也不用烦恼,请抱着休闲的心态来学习吧~
MVC 是最早的明确把 Android 页面框架划分为 视图层(View)、逻辑层(Controller)和 数据模型层(Model) 的架构,它们的关系如下图所示:
逻辑单元的流动过程是:
MVC 的缺点有:
Activity
/ Fragment
为载体,它们正好又是视图UI,所以页面逻辑稍微复杂一点,就会导致 Activity
的代码臃肿膨胀,不好维护,代码可读性差MVP 的好处就是把 MVC 的缺点解决了, View层 和 Model层 不直接耦合,将逻辑层改了个名字,叫 Presenter
,如下图所示:
逻辑单元的流动过程是:
MVP 的缺点是:
Jetpack 出来后,它通过 LifeCycle
为 Presenter 赋予了能感知 View 生命周期的能力,并改了个名字叫 ViewModel
。
Jetpack 甚至直接提供了 ViewModel
类、 LiveData
类等来让我们使用,非常方便。
class MyViewModel : ViewModel() {
override fun onCleared() {
// 释放资源
}
}
在没有使用 DataBinding
时, MVVM 和 MVP 其实是差不多的,如下图所示:
无 DataBinding 版的缺点是:
DataBinding
版的 MVVM 没有做到这点LiveData
, 每个 LiveData
都可以看做是触发 View 更新的一个刷新点,假设 UI 展示出现异常,我们需要从众多刷新点中找到有问题的那一个,调试上可能会比较麻烦~MVVM的核心是双向绑定,也就是 View 和 ViewModel 的一种自动绑定,这种能力是:
View
可以直接触达 ViewModel
逻辑,而无需通过在代码中写 setClickListener{ viewModel.onClick() }
等逻辑ViewModel
的变化可以直接更新 View
, 而无需通过在 View
代码中写 setText=xxx
、setXXX
等逻辑总结就是加了 DataBinding
/ ViewBinding
后 ,好处就是 Activity
/ Fragment
可以少写一些诸如 findViewById
、setXXX
等的样板代码:
MVVM 有 DataBinding 版的缺点是:
DataBinding
的代码,往往都很不直观,跳来跳去的看得费神DataBinding
会直接注入到 View 的 XML 文件中,这使得这个 View 不好在其它地方被直接复用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 具备样什么特性。
MVI 的核心是:唯一可信数据源的单向流动。
数据的单向流动,其实就是不想让数据双向流动。
数据的双向流动就是使用 DataBinding
那样:数据模型的变化会同步到视图上,视图上的操作同时也会同步到数据模型上。
而数据的单向流动,强调的是数据的源头只有一个,目的地只有一个,数据的流向是易追踪的。
这两者其实可以下面这种简单的方式来区分:
View
观察 ViewModel
(视图数据)来更新自身,那么数据就是单向流动的View
观察, ViewModel
的更新能够自动触发 View
的更新,那么数据就是双向绑定的那其实 MVC、MVP、MVVM(无 DataBinding
) 都是数据单向流动的框架。
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 更新了!)
MVI 将架构分成了三个部分:
I - Intent
意图:它是简单描述用户与App交互时产生的一个动作或命令。例如按钮的点击,页面的滑动切换V - View
视图:实际的 UI 组件M - ViewModel + Model
视图模型 + 数据层:该层就是数据层,它一大一小有两层:
ViewModel
: 例如需要展示在 TextView
上的文案、ImageView
上的图片资源等Repository / DataStore
: 例如需要通过数据库或网络请求得到的数据,它一般作为 ViewModel
的数据源它们的关系如下所示:
对于 V 层和 M层 都是比较好理解的,而 I层 和我们之前所遇到的 Controller
、 Presenter
有所不同,它不是一个单独具体的实体,而是一个描述数据流动的模型。
那么它要如何表示呢?
MVI 认为只要视图还存在,用户就会源源不断地和视图界面进行交互,所以在 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 框架上开始流行的原因。
我们可以基于 MVP 或者无 DataBinding
版本的 MVVM 搞出 MVI模式。 由于我们需要使用响应式编程,而 ViewModel 提供了协程作用域,方便于我们使用 Flow
,所以 MVVM 相较于 MVP 能够更舒服的做出 MVI。所以基本上所有的 MVI 架构都是用 MVVM 来写的,即使用 ViewModel 而非 Presenter。
意图描述用户交互,所以我们可以把所有用户有关的操作都写出来,并用 场景名+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() {
...
}
}
在 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)
}
}
}
我们通过归纳新闻页的页面状态,可以分成 初始态、加载中、加载成功、加载失败 四个状态,那么我们将状态收归到 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)
}
}
}
}
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()
}
无论是 MVC、MVP、MVVM 还是 MVI,它们的共同点都是有 M层 和 V层。
所以这些架构的区别,也就是 MV 后面那个字母的区别,但万变不离其中的是:它们的作用是描述 Model 和 View 之间的关系。
Intent
意图,就是这个数据流的起点,即生产者,而 View
则是数据流的重点,即消费者State
刷新,这个 State
就是唯一可信数据源, 仅依赖单一状态的 UI 是更好测试的Flow
(响应式模型),所以我们一般用 MVVM 来写 MVI综上所述,MVI 和 MVP、MVVM 这两位并不是一个维度的东西。MVI 强调数据的流动方向,而后两者则是强调结构分层。
MVI 的缺点是:
但是这些缺点, 大前端早就已经克服了,Android 肯定也有对应的实现,这里就不再赘述,后面遇到了再记录。
MVI(Model-View-Intent) Pattern in Android
官网