前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文了解MVI架构,学起来吧~

一文了解MVI架构,学起来吧~

作者头像
黄林晴
发布2022-05-10 15:00:22
1.1K0
发布2022-05-10 15:00:22
举报
文章被收录于专栏:代码男人代码男人

前言

大约在去年11月份,Google将官方网站上推荐的MVVM架构悄悄替换成了MVI架构。参考了官方与许多前辈的分享,便有了此文。不过下面的再前言应当是每个读者心中所需要认定的。

再前言

总览我所有的博客,我很少写关于架构模式相关的文章。因为我觉得:

不管是从刚开始所使用的MVP、MVVM再到现在Google官方所推荐的MVI架构,我希望各位读者千万不要将教条主义当真理。官方推荐了MVVM就马上去踩MVP,官方推荐了MVI就马上去踩MVVM,甚至使用MVVM的开发者会鄙视使用MVP的,使用MVI的开发者会鄙视使用MVVM,这一点真是滑稽。

其实完全没必要如此,符合项目本身才是最好的架构。许多技术交流群中趣称:“Google工程师为了KPI 苦了广大开发者”。这让我想到“大明风华”中的一句台词

MVI架构

好了,废话说了这么多,我们来看MVI架构是什么样子的,直接看官方网站的一张图,如下所示:

MVI中 分为UI层、网域层、与数据层,我造个词叫他UDD,其中网域层可有可无,网域层我们最后再来看。我这里不会再一一截图来展示UI层怎么样、数据层怎么样,可直接看官网。(其实相比较于MVVM基本没变化)

MVI中的I是Intent即为用户意图,如点击事件、刷新等都是Intent。那么MVI到底解决了MVVM中的什么问题呢?

我现在所给出的答案就是: 集中管理State用户意图管理 (单数据流),单数据流就是状态向下流动、事件向上流动的模式。接下来我们着重来看这两点。

集中管理State

在MVVM样式的代码中,以网络请求功能为例,UI状态分为正在加载、加载成功与加载失败,为了监听UI状态,我们会在Viewmodel中定义变量监听,代码如下所示:

代码语言:javascript
复制
   /***
     * 是否正在加载
     */
    private val isLoding = MutableSharedFlow<Boolean>()
    val _isLoding: SharedFlow<Boolean>
        get() = isLoding


    /***
     * 加载成功
     */
    private val loadSuccess = MutableSharedFlow<ReqData>()
    val _loadSuccess: SharedFlow<ReqData>
        get() = loadSuccess


    /***
     * 加载失败
     */
    private val loadFailed = MutableSharedFlow<Exception>()
    val _loadFailed: SharedFlow<Exception>
        get() = loadFailed

由于不能破坏数据的封装性,所以我们要定义一个私有的不可变的MutableSharedFlow用于在Viewmodel赋值,再对外暴露一个不可变的用于在UI层监听。UI中的监听代码如下所示:

代码语言:javascript
复制
viewModel._isLoding.collect {
   if (it){
     //显示弹窗
   }else{
    //关闭敢闯
   }
}
viewModel._loadSuccess.collect {
      //加载成功显示数据               
}
viewModel._loadFailed.collect {
   //加载失败逻辑处理                 
}

这种方式的缺点,相信一眼就可以看出,就是页面有多少种状态,就要定义多少个类似的变量,模板代码过多,且不利于维护。

所以,UI State集中管理就是将所有状态写在一个类中,可以是密封类或者普通类都可以,各有各的好处,这里我们使用密封类定义,新建MainUiState类,代码如下所示:

代码语言:javascript
复制
sealed class MainUiState {
    /**
     * 正在加载
     */
    object isLoading : MainUiState()

    /**
     * 请求失败
     * @param error 异常日志
     */
    data class loadError(val error: Exception) : MainUiState()

    /**
     * 请求成功
     * @param reqData 返回数据
     */
    data class loadSuccess(val reqData: ReqData):MainUiState()

}

我们在MainUiState中提前定义好了UI的各种状态,怎么样,有没有瞬间回到MVP时代在View中提前定义好各种接口的感觉。

修改ViewModel中的代码如下所示:

代码语言:javascript
复制
private val _state = MutableSharedFlow<MainUiState>()
val state: SharedFlow<MainUiState>
    get() = _state

 这样只需要定义一个state变量,在Activity监听如下所示:

代码语言:javascript
复制
lifecycleScope.launch {
            try {
                viewModel.state.collect {
                    when (it) {
                        MainUiState.isLoading -> {}
                        is MainUiState.loadSuccess -> {}
                        is MainUiState.loadError -> {}
                    }
                }
            } catch (e: Exception) {

            }
        }

这样一来,减少了ViewModel的模板代码,但是这里需要注意的是,集中管理是相对的,没必要把无论是否相关的状态管理都放在一个密封类中。

当state中的状态很多时,可能会由于某个属性改变而频繁刷新视图,开发者没办法判断值是否改变,针对这种情况我们可以使用distinctUntilChanged方法处理,代码如下所示:

代码语言:javascript
复制
 viewModel.state.distinctUntilChanged().collect {
            when (it) {
                 MainUiState.isLoading -> {}
                 is MainUiState.loadSuccess -> {}
                 is MainUiState.loadError -> {}
                }
 }

用户意图管理

在使用MVI之前,刷新、点击事件操作我们都是在Activity中直接调用ViewModel的方法,代码如下所示:

代码语言:javascript
复制
binding.btnRefresh.setOnClickListener {
  //刷新
  viewModel.refresh()
}

binding.btnLoadData.setOnClickListener {
  //查询数据
  viewModel.loadData()
}

其实,上面这种方式也没有什么缺陷,在单项数据流模式中,Activity向ViewModel发送Intent事件,从而ViewModel集中处理用户操作。可以简单的理解为用户事件也统一管理、统一处理。

首先我们定义MainIntent类,定义好页面中的操作,代码如下所示:

代码语言:javascript
复制
sealed class MainIntent {

    /**
     * 刷新
     */
    object refresh : MainIntent()

    /**
     *查询数据
     */
    object loadData : MainIntent()

}

在ViewModel中定义一个userIntent变量用于接收用户事件,代码如下所示:

代码语言:javascript
复制
/**
 * 接收事件
 */
private val userIntent = MutableSharedFlow<MainIntent>()

  将viewModel中的refresh、loadData方法设为私有,并新建一个dispatch方法用于分发用户事件,代码如下所示:

代码语言:javascript
复制
 /**
   * 分发用户事件
   * @param viewAction
 */
 fun dispatch(viewAction: MainIntent) {
     try {
         viewModelScope.launch {
            userIntent.emit(viewAction)
         }
      } catch (e: Exception) {
    }
 }

 并在ViewModel中检测userIntent的变化,触发对应请求,代码如下所示:

代码语言:javascript
复制
 init {
        viewModelScope.launch {
            userIntent.collect {
                when (it) {
                    is MainIntent.refresh -> refresh()
                    is MainIntent.loadData -> loadData()
                    else -> {}
                }
            }
        }
    }

这样,在Activity中,只需要传递用户意图即可,代码如下所示:

代码语言:javascript
复制
binding.btnRefresh.setOnClickListener {
    //刷新
    viewModel.dispatch(MainIntent.refresh)
}

binding.btnLoadData.setOnClickListener {
    //查询数据
    viewModel.dispatch(MainIntent.loadData)
}

这样一来,将事件管理、状态转化都放在了ViewModel中,这样体现的好处就是保证数据一致性,不通过页面也可以清晰的看到有哪些事件、状态。当XML替换为Compose的时候,就可以只注重页面的实现了?

关于网域层 

关于网域层的介绍很少,基本都是按照官方意思概括。因为他是可有可无的,甚至说对一般App来说都是不需要的。但是网域层到底是什么东西呢?为什么他是可有可无的呢?这里我说一下自己的理解。

网域层是位于页面层和数据层之间的,也就是Activity与Respository层之间的。可以负责封装复杂的业务逻辑,或者多个ViewModel重复使用的简单业务逻辑。

我对网域层的理解,类似设计模式中的 ”门面模式“,关于门面模式,后面我会在单独写一篇文章介绍。

简单的说 比如现在有ARespository和BRespository,分别查询数据A和数据B,在业务A、B模块中需要各自查询数据A、B,在业务C模块和D模块中都需要将A、B数据通过业务逻辑处理(如拼接、去重等操作)后显示。此时这部分业务逻辑是没办法直接写在A或BRespository中的,但又是一个重复业务逻辑,所以我们抽取出一个网域层,用于接收A、B层的数据,将数据处理后返回给UI层。同时还可能有其他业务模块的数据源来自CRespository和ARespository,此时再抽取一个网域层用于单独处理数据。这样一来,避免了代码重复、将部分重复逻辑抽取到网域层减轻其他层的负担。

写在最后

相信看了这篇文章,你对在Android中如何使用MVI有了一定的了解,但一定要切记,架构没有好坏之分,适合项目本身的架构就是好架构~ 

期待我们下篇文章再见~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-04-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 再前言
  • MVI架构
  • 集中管理State
  • 用户意图管理
  • 关于网域层 
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档