这些年来,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 肯定也有对应的实现,这里就不再赘述,后面遇到了再记录。