Android MVI 架构学习

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MVI 架构学习相关的知识,希望对你有一定的参考价值。

文章目录

1. 概述

1.1 android 架构的背景

这些年来,Android 上发展了多种主流架构,从最开始的 MVC,到 CleanMVP,再到现在最火热的 MVVM,可以说架构发展一直很卷,这不,MVVM 还没有用个几年呢, MVI 就出来了。

要说 Android 架构卷,实则不然,上面说的这些架构其实根本不是来自 Android 的,而是源自于 Web,即大前端, Web 由于其自身特性(还不算完全成熟,业务多且杂,热部署等),版本迭代速度巨快,技术的更新迭代也因此很快,上面这些架构最早就是在前端所应用和发展出来的, 而移动端则是直接抄来,跟着Web的步伐前进。

所以 MVI 显然不是 Android 架构的终点,说不定明年 Web 上就又弄出一个新玩意把它取代了。不学习 MVI 并不会让我们落伍,现阶段 MVPMVVM 足以解决Android所有的业务场景。

但是学习 MVI 有这么几个好处:

  1. 装杯
    新鲜的架构会让代码逼格提升,展示代码时,你可以用 “唯一可信刷新点”、“数据单向流动” 等词语来装杯,而且 MVI 会使用协程Flow,Flow 可是 Google 这两年很推荐的框架呢
  2. 官方认定
    Android 官网在去年就已经有推荐使用 MVI 的文章了, 在 Google 开发者大会中也有专门直播介绍 MVI 架构,所以它是官方认可的架构,至少不会那么容易被淘汰,而且面试也有可能会问到
  3. 素材好找,容易引用
    MVI 并不会引入什么三方库。比起具体的架构形态,它的形态更像是一种设计思想,基于已有的 MVPMVVM 进行调整,也能搞出 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=xxxsetXXX 等逻辑

总结就是加了 DataBinding / ViewBinding 后 ,好处就是 Activity / Fragment 可以少写一些诸如 findViewByIdsetXXX 等的样板代码:

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层 和我们之前所遇到的 ControllerPresenter 有所不同,它不是一个单独具体的实体,而是一个描述数据流动的模型。

那么它要如何表示呢?

2.4 Intent和响应式编程

MVI 认为只要视图还存在,用户就会源源不断地和视图界面进行交互,所以在 UI 的生命周期内会产生很多用户操作所产生出的数据。

这些源源不断数据则可以用数据流来表示,数据流模型可以简单的描述为在生产者-消费者模型下,生产者作为上游可以不断产生数据,下游的消费者接收这些数据并进行处理消费,而用户的交互和程序的响应正好能对应这个模型:

  • 生产者 —— 数据流的起点 —— 用户的交互,例如点击某个按钮, 它能产生一个信号,来让 App 去请求网络数据
  • 消费者 —— 数据流的终点 —— UI, 将网络请求回来的数据进行层层处理,然后显示在屏幕上

Intent 就是生产者, 即数据流的起点,例如下面代码就是用户由交互产生的一个意图:

button.setOnClickListener 
    // 产生一个意图
    viewModel.sendIntent(BUTTON_CLICK)

其次,处理数据流的模型是响应式编程模型, 我们知道一些专门的框架,例如 RxJavaFlow,这里不再具体介绍了…

所以这里的意图作为数据流的起点,也应该使用响应式编程的做法,所以我们一般看到的示例代码都是用 协程的 ChannelStateFlow 举例子的(这里不了解的同学可以看这篇文章:深潜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 架构学习的主要内容,如果未能解决你的问题,请参考以下文章

Android MVI 架构简介

Android MVI 架构简介

Android MVI 架构学习

Android MVI 架构学习

Android MVI 架构学习

Android真的推荐用MVI模式?MVI和MVVM有啥区别?