浅尝MVI

356 阅读4分钟

在这里先介绍一下写这个开源项目的原因,主要原因是想让自己封装的玩具有一个实际的应用,看看在实际开发中会有什么问题,看看哪些还需要完善。事实证明也确实如此,在开发的过程中发现自己封装的玩具还确实有很多不足。这些玩具包括一些基础架构,也包括一些插件,还有一些好的想法。

首先说下应用的技术栈

  • 用kts和buildSrc进行了依赖版本库的统一控制
  • 用协程+flow+retrofit 进行了一个网络库的封装
  • 用Hilt对ViewModel ,ApiService ,Repository 进行了依赖注入
  • 图片加载库使用的是Glide,后续可能会考虑Coil
  • 整体架构方面是在MVVM的基础上,进行了MVI架构的尝试。

现在这个项目还并没有开发完成,有一些功能还没有完善,也有一些问题还没来得及修复,本来我是打算想等待开发完成之后再进行开源,但是我感觉在开发过程中开源,可以更快的吸收更好的建议,更好的想法,可以在开发过程中就可以将这些好的想法进行应用。

现在来说说细节吧

依赖管理方面是使用buildSrc+kts,在项目编译的时候 首先会去执行buildSrc下面的内容,这样就可以先把依赖常量在这里进行定义,然后直接在kts里面进行引用

object Libraries {
   const val hiltlifecycleviewmodel = "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
   const val hiltandroid = "com.google.dagger:hilt-android:2.36"
   const val daggercompiler = "com.google.dagger:hilt-android-compiler:2.36"
   const val hiltcompiler = "androidx.hilt:hilt-compiler:1.0.0"
   const val base = "com.xl:base:1.1.3"
}
   implementation(Libraries.hiltlifecycleviewmodel)
   implementation(Libraries.hiltandroid)
   implementation(Libraries.base)
   kapt(Libraries.daggercompiler)
   kapt(Libraries.hiltcompile

这里选择kts 的原因它如果有写法错误可以及时作出提醒,就像我们写业务代码一样,不用像groovy那种,只有去sync 的时候 才会报出错误。点击引用跳转也相对方便一些。

架构层次还是按照 View -> ViewModel ->Repository 这样的结构来的

按照我以前的处理操作,可能会像这样操作

进行数据的刷新,在view层 直接获取第一页的数据

mainViewModel.getArc(0)

ViewMoodel 层 从Repository 获取数据,然后交给livedata

  private val _data = MutableLiveData<ArticleBean?>()
  val data: LiveData<ArticleBean?> = _data
​
   fun getArc(page: Int) {
       viewModelScope.launch {
           _data.value = repository.getArticles(page = page).data
      }
  }

最后在View层对数据进行订阅,然后更新UI。

mainViewModel.data.observe(this){it->
          .......   
  //更新UI                              
}       

这样做有问题吗?这样做是没有问题的。

但是当这个页面有N多个请求的时候,想想会怎么样?

可能就会发生View层和ViewModel层交互比较凌乱的情况,每个暴露的livedata 也都要定义两个,可变的内部使用,不可变的内部订阅。View层也会出现各种订阅满天飞的情况。

为了解决这个问题,所以就将用户的操作(例如刷新数据),和UI状态(例如:返回数据刷新UI )抽象起来,这里我是使用了flow 去实现

image-20220115173934575.png

数据的订阅只需要关心ViewState 就可以了。

MVI 中的 I 就是Intent,这并不是跳转所用的Intent,它的意思是一个时间,例如刷新数据

在这里我定义的事件是ViewEvent,UI状态我定义的是ViewState

这是我定义的ViewState

data class ViewState(
   var homeInfo: HomeInfo? = null,
   var rankingInfo: RankingInfo? = null,
)

然后在View层直接这样订阅就可以:

    viewModel.state.collectHandlerFlow(this) { state ->
           state.homeInfo?.let { it ->
            //更新UI
          }
           state.rankingInfo?.let { 
            //更新UI    
          }
      }

关于collHandlerFlow 我是做了一下这样的封装:

inline fun <T> StateFlow<T>.collectHandlerFlow(
   lifecycleOwner: LifecycleOwner,
   crossinline action: (T) -> Unit,
) =  lifecycleOwner.lifecycleScope.launch {
       lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { T -> action(T) } }
  }

关于ViewEvent ,我是选择使用了Channel

在Channel中,每个事件被传递给一个订阅者。试图在没有订阅者的情况下发布事件,一旦Channel缓冲区变满就会暂停,等待订阅者出现。发布的事件不会被丢弃。

   private val pendingActions = Channel<ViewEvent>(Channel.BUFFERED)
​
   init {
       viewModelScope.launch {
           pendingActions.consumeAsFlow().collect { action ->
               when (action) {
                   ViewEvent.Refresh -> getHome(0)
              }
          }
      }
  }
​
   fun submitAction(action: ViewEvent) {
       viewModelScope.launch {
           if (!pendingActions.isClosedForReceive) {
               pendingActions.send(action)
          }
      }
  }

在view层发送刷新事件

viewModel.submitAction(ViewEvent.Refresh)

这样就把用户操作的事件和UI状态做了一个整体的封装,对数据的关注点进行了集中,只需要关心状态和事件就可以。

这只是一套简单的封装,其中还有很多的不足,在以后的一些时间 会去慢慢完善。

在这个项目中,我是将一些基础共用 和 base 方面的东西,统一封装在了另外一个library中。在这个library 中就是单纯的写common 和 base,和一些基本的依赖,然后上传到了我的个人仓库中,这样做主要是因为 我可以将这套东西很快的集成到另外一个新项目中。

后面的计划是在功能完善和bug修复上,然后会尝试使用一下coil框架,做简单尝试;后面会尝试使用Compose重写一版。

暂时先水到这里吧,等有新的想法会再继续分享。有问题或者其他更好的建议,欢迎留言。

OpenEye:github.com/WngYilei/Op…