手把手教你搭建 Android MVI架构: MVI + kotlin + Flow

打印 上一主题 下一主题

主题 952|帖子 952|积分 2856

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x


一、什么是MVI

Android MVI是一种用于构建Android应用程序的架构模式,其焦点思想在于实现单向数据流和唯一可信数据源。在MVI架构中,应用程序的状态管理得到简化,并且用户界面与业务逻辑之间的交互更加清晰和规范。
二、MVI组成

MVI由Model、View和Intent三个焦点组件组成:
   

  • Model:代表数据模型,负责存储应用程序的状态。它是唯一可信的数据源,意味着应用程序的所有状态都集中在这里管理,避免了状态不一致的标题。
  • View:即用户界面,负责展示Model中的状态,并相应用户的使用。当用户与界面举行交互时,会产生相应的Intent。
  • Intent:表示用户的意图或使用。它是View向Model通报信息的方式,告诉Model用户想要执行的使用或盼望到达的状态。
  在MVI架构中,数据运动是单向的:View产生Intent,Intent通报给Reducer(一个处置惩罚Intent并更新Model状态的函数),Reducer根据Intent生成新的Model状态,并将这个状态发送回View举行渲染。这种单向数据流有助于简化状态管理,并使得代码更加可预测和易于维护。
MVI架构还强调代码分层和清晰的责任划分。ViewModel无需关心View如何触发和更新,它只需要维护Intent和State。View与ViewModel的交互更加规范,使用Kotlin的密封类特性来封装Intent和State,使得代码更加规范、整洁和易读。

从图中可以看到,
   

  • 数据从Data Layer -> ViewModel -> UI,数据是单向运动的。ViewModel将数据封装成UI State传输到UI elements中,而UI elements是不会传输数据到ViewModel的。
  • UI elements上的一些点击或者用户事件,都会封装成events事件,发送给ViewModel。
  • ViewModel更新状态:

    • 在处置惩罚事件的过程中,ViewModel 的内部状态(即 UI State)可能会发生变化。
    • 这些状态变化通常是基于用户使用和业务逻辑的结果。

  • 通知Vew UI更新:

    • 一旦 ViewModel 的状态发生变化,它需要通知 UI 举行相应的更新。
    • 这通常是通过某种机制完成的,好比使用观察者模式(如 RxSwift、Kotlin Flow 等),或者通过数据绑定(如 Flutter 的数据流或 Jetpack Compose 的状态持有者)。
    • 当 UI 吸收到 ViewModel 的状态更新时,它会根据新的状态重新渲染自己。

  通过这样的流程,ViewModel 在应用程序中充当了一个中介角色,它吸收 UI 的事件,处置惩罚这些事件并更新状态,然后通知 UI 举行相应的渲染。这种单向数据流的方式有助于保持代码的可维护性和可测试性,同时也有助于实现更清晰的组件间解耦。
三、MVI框架搭建

  搭建步骤:
   

  • 定义UI State、events
  • 构建UI State单向数据流UDF
  • 构建事件流events
  • UI State的订阅和发送
  3.1 定义UIState、events

  1. import androidx.annotation.Keep
  2. @Keep
  3. interface IUiIntent
复制代码
  1. import androidx.annotation.Keep
  2. @Keep
  3. interface IUiState
复制代码
然后根据详细逻辑定义页面的UIState和UiIntent。
  1. data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState
复制代码
  1. sealed class BannerUiState {
  2.     object INIT : BannerUiState()
  3.     data class SUCCESS(val models: List<Banner>) : BannerUiState()
  4. }
复制代码
  1. sealed class DetailUiState {
  2.     object INIT : DetailUiState()
  3.     data class SUCCESS(val articles: Article) : DetailUiState()
  4. }
复制代码
  1. sealed class MainIntent : IUiIntent {
  2.     object GetBanner : MainIntent()
  3.     data class GetDetail(val page: Int) : MainIntent()
  4. }
复制代码
通过MainState将页面的不同状态封装起来,从而实现唯一可信数据源
3.2 构建事件流

在ViewModel中使用StateFlow构建UI State流


  • _uiStateFlow用来更新数据
  • uiStateFlow用来袒露给UI elements订阅
  1. abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {
  2.     private val _uiStateFlow = MutableStateFlow(initUiState())
  3.     val uiStateFlow: StateFlow<UiState> = _uiStateFlow
  4.     protected abstract fun initUiState(): UiState
  5.     protected fun sendUiState(copy: UiState.() -> UiState) {
  6.         _uiStateFlow.update { copy(_uiStateFlow.value) }
  7.     }
  8. }
复制代码
  1. class MainViewModel : BaseViewModel<MainState, MainIntent>() {
  2.     override fun initUiState(): MainState {
  3.         return MainState(BannerUiState.INIT, DetailUiState.INIT)
  4.     }
  5. }
复制代码
3.3 构建事件流
在ViewModel中使用 Channel构建事件流
   有人好奇这里为啥用Channel,而不用SharedFlow或者StateFlow?
  Channel就像一个队列一样,适合实现单个生产者和单个消费者之间的通讯,而 SharedFlow 更适合实现多个观察者订阅同一数据源。而这里的Intent事件更像前者,各个协程生产出不同的Intent事件通过Channel发送给ViewModel,然后在ViewModel中集中处置惩罚消费。
  

  • _uiIntentFlow用来传输Intent
  • 在viewModelScope中开启协程监听uiIntentFlow,在子ViewModel中只用重写handlerIntent方法就可以处置惩罚Intent事件了
  • 通过sendUiIntent就可以发送Intent事件了

  1. abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {
  2.     private val _uiIntentFlow: Channel<UiIntent> = Channel()
  3.     val uiIntentFlow: Flow<UiIntent> = _uiIntentFlow.receiveAsFlow()
  4.    
  5.     fun sendUiIntent(uiIntent: UiIntent) {
  6.         viewModelScope.launch {
  7.             _uiIntentFlow.send(uiIntent)
  8.         }
  9.     }
  10.     init {
  11.         viewModelScope.launch {
  12.             uiIntentFlow.collect {
  13.                 handleIntent(it)
  14.             }
  15.         }
  16.     }
  17.     protected abstract fun handleIntent(intent: IUiIntent)
复制代码
  1. class MainViewModel : BaseViewModel<MainState, MainIntent>() {
  2.     override fun handleIntent(intent: IUiIntent) {
  3.         when (intent) {
  4.             MainIntent.GetBanner -> {
  5.                 requestDataWithFlow()
  6.             }
  7.             is MainIntent.GetDetail -> {
  8.                 requestDataWithFlow()
  9.             }
  10.         }
  11.     }
  12. }
复制代码
3.4 UI State的订阅和发送

3.4.1 订阅UI State

在Activity中订阅UI state的变化

  • 在lifecycleScope中开启协程,collect uiStateFlow。
  • 使用map 来做局部变量的更新
  • 使用distinctUntilChanged来做数据防抖
  1. class MainActivity : BaseMVIActivity() {
  2.     private fun registerEvent() {
  3.         lifecycleScope.launchWhenStarted {
  4.             mViewModel.uiStateFlow.map { it.bannerUiState }.distinctUntilChanged().collect { bannerUiState ->
  5.                 when (bannerUiState) {
  6.                     is BannerUiState.INIT -> {}
  7.                     is BannerUiState.SUCCESS -> {
  8.                         bannerAdapter.setList(bannerUiState.models)
  9.                     }
  10.                 }
  11.             }
  12.         }
  13.         lifecycleScope.launchWhenStarted {
  14.             mViewModel.uiStateFlow.map { it.detailUiState }.distinctUntilChanged().collect { detailUiState ->
  15.                 when (detailUiState) {
  16.                     is DetailUiState.INIT -> {}
  17.                     is DetailUiState.SUCCESS -> {
  18.                         articleAdapter.setList(detailUiState.articles.datas)
  19.                     }
  20.                 }
  21.             }
  22.         }
  23.     }
  24. }
复制代码
3.4.2 发送Intent

直接调用sendUiIntent就可以发送Intent事件
  1. button.setOnClickListener {
  2.     mViewModel.sendUiIntent(MainIntent.GetBanner)
  3.     mViewModel.sendUiIntent(MainIntent.GetDetail(0))
  4. }
复制代码
3.4.3 更新Ui State

调用sendUiState发送Ui State更新
需要注意的是: 在UiState改变时,使用的是copy复制一份原来的UiState,然后修改变更的值。这是为了做到 “可信数据源”,在定义MainState的时间,设置的就是val,是为了避免多线程并发读写,导致线程安全的标题。
  1. class MainViewModel : BaseViewModel<MainState, MainIntent>() {
  2.     private val mWanRepo = WanRepository()
  3.     override fun initUiState(): MainState {
  4.         return MainState(BannerUiState.INIT, DetailUiState.INIT)
  5.     }
  6.     override fun handleIntent(intent: IUiIntent) {
  7.         when (intent) {
  8.             MainIntent.GetBanner -> {
  9.                 requestDataWithFlow(showLoading = true,
  10.                     request = { mWanRepo.requestWanData() },
  11.                     successCallback = { data -> sendUiState { copy(bannerUiState = BannerUiState.SUCCESS(data)) } },
  12.                     failCallback = {})
  13.             }
  14.             is MainIntent.GetDetail -> {
  15.                 requestDataWithFlow(showLoading = false,
  16.                     request = { mWanRepo.requestRankData(intent.page) },
  17.                     successCallback = { data -> sendUiState { copy(detailUiState = DetailUiState.SUCCESS(data)) } })
  18.             }
  19.         }
  20.     }
  21. }
复制代码
其中 requestDataWithFlow 是封装的一个网络请求的方法
  1. protected fun <T : Any> requestDataWithFlow(
  2.     showLoading: Boolean = true,
  3.     request: suspend () -> BaseData<T>,
  4.     successCallback: (T) -> Unit,
  5.     failCallback: suspend (String) -> Unit = { errMsg ->
  6.         //默认异常处理
  7.     },
  8. ) {
  9.     viewModelScope.launch {
  10.         val baseData: BaseData<T>
  11.         try {
  12.             baseData = request()
  13.             when (baseData.state) {
  14.                 ReqState.Success -> {
  15.                     sendLoadUiState(LoadUiState.ShowMainView)
  16.                     baseData.data?.let { successCallback(it) }
  17.                 }
  18.                 ReqState.Error -> baseData.msg?.let { error(it) }
  19.             }
  20.         } catch (e: Exception) {
  21.             e.message?.let { failCallback(it) }
  22.         }
  23.     }
  24. }
复制代码
  1. <strong>至此一个MVI的框架基本就搭建完毕了</strong>
复制代码
以为我写的好的兄弟可以动动发财的小手帮我点个赞 谢谢!!!!!
源码链接地点:https://download.csdn.net/download/a546036242/88971170

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大连密封材料

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表