说在前头:

纪晓岚问和珅,为何他们往哀鸿粥里掺沙子,和珅道:“你是有所不知啊,如不掺沙子,哀鸿怕是一口粥也喝不上啊”。

同理,架构的存在是为 “在实践开发过程中消除不可预期问题”,而非为架构而架构。

为使架构组件真实能在团队中遍及,乃至毕竟有用达成 “消除大部分不可预期问题” 意图,本文采用 “淡化理论概念 + 规划简明易懂” 办法,让团队新手老手都能因为 “这结构好懂、简洁、用着舒畅”,而自可是然效仿和运用。

本文假设您已具有 State、Event、照应式编程、BehaviorSubject、PublishSubject、函数式编程、纯函数、副作用、MVI、软件工程、规划方式准则、一起性问题 等前置知识,且在团队中推行 MVI 遭遇晦气,想就近找到平替方案。

通过本文可快速了解:

1.为何运用 MVI,是否非用不可,

2.为何毕竟考虑 SharedFlow 完结,

3.repeatOnLifecycle + SharedFlow 完结 MVI 思路

前置知识

上一期《关于 MVI,我想聊的更了解些》,我们已烘托如下信息:

1.照应式编程暗示人们 应当总是向数据源央求数据,并在指定查询者中照应数据的改变

2.照应式编程的优点是 便于检验,有输入必有回响

3.照应式编程 存在 “多个粘性查询者回推不符预期数据” 的缝隙

4.MVI 便是 通过 “聚合页面状态” 消除该缝隙

5.鉴于 “照应式编程” 便于检验,官方出于齐备性考虑,也是以照应式编程作为架构示例。

6.因为 Kotlin 抹平语法杂乱度,便于照应式编程,且 Kotlin 开发者更简单跟着官方文档走,接受这套开发方式,乃至 有机遇踩坑,且有动力通过 MVI 改善

7.Android 开发者 70% 仍是纯 Java,照应式编程在 Android Java 开发者中的推行不太抱负。

为何运用 MVI,是否非用不可

所以至此,第一个问题的答案呼之欲出,

因为对一部分隔发者来说,照应式编程很香,但又存在缝隙,即部分 BehaviorSubject 结构存在过度规划,导致存在 “多个粘性查询者不符预期回推” 的缝隙,所以需求 MVI 出马处理。

注:什么是过度规划,如何避免?详细见上期解析,本文不再累述。

那有人可能会问,已然部分 BehaviorSubject 结构过度规划,那替换成没有过度规划的 BehaviorSubject,比如 ObservableField 不就可以了,

可以是可以,不过也看状况,MVI 天然合适与 Jetpack Compose 搭配,

如果是运用 Jetpack Compose,就用不上 ObservableField,只能运用 LiveData/StateFlow 来回推 UiStates,也即只能通过 MVI 来消除缝隙,难有别的平替方案。

所以如果暂不运用 Jetpack Compose,依据上期的剖析易知,只要消除过度规划,就能从源头上把问题处理,无所谓开发者用不用 MVI。

鉴于上期文末已共享 MVI 最小本钱平替方案,本文直接从 “规划方式准则” 动身,探求一种更加普适的方案,信赖阅读后你会耳目一新。

MVI 经典模型

1.创建一个 UiStates,反映其时页面的全部状态。

data class UiStates {
  val weather : Weather,
 val isLoading : Boolean,
 val error : List<UiEvent>,
}

2.创建一个 Intent,用于发送央求时带着参数,和指明其时想履行的业务。

sealed class MainPageIntent {
  data class GetWeather(val cityCode) : MainPageIntent()
}

3.创建一个 Actions,用于 reduce 其时业务的 partialChange 并生成新的 UiStates。

sealed class MainPageActions {
 fun reduce(oldStates : UiStates) : UiStates {
  return when(this){
   Loading -> oldStates.copy(isLoading = true)
   is Success -> oldStates.copy(isLoading = false, weather = this.weather)
   is Error -> oldStates.copy(isLoading = false, error = listOf(UiEvent(msg)))
   }
  }
 
  object Loading : MainPageActions()
  data class Success(val weather : Weather) : MainPageActions()
  data class Error(val msg : String) : MainPageActions()
}

4.创建其时页面运用的 MVI-Model。

class MainPageModel : MVI_Model<UiStates>() {
 private val _stateFlow = MutableStateFlow(UiStates())
 val stateFlow = _stateFlow.asStateFlow
 
 private fun sendResult(uiStates: S) = _stateFlow.emit(uiStates)
 
 fun input(intent: Intent) = viewModelScope.launch{ onHandle() }
 
 private suspend fun onHandle(intent: Intent) {
  when(intent){
     is GetWeather -> {
    sendResult(MainPageActions.Loading.reduce(oldStates)
       val response = api.post()
       if(response.isSuccess) sendResult(
     MainPageActions.Success(response.data).reduce(oldStates)
       else sendResult(
     MainPageActions.Error(response.message).reduce(oldStates)
     }
   }
  }
}

5.创建 MVI-View,并在 stateFlow 中照应 MVI-Model 数据。

class MainPageActivity : Android_Activity(){
  private val model : MainPageModel
  fun onCreate(){
  lifecycleScope.launch {
  repeatOnLifecycle(Lifecycle.State.STARTED) {
   model.stateFlow.collect {uiStates ->
       progressView.setProgress(uiStates.isLoading)
       tvWeatherInfo.setText(uiStates.weather.info)
     ...
    }
  }
  model.input(Intent.GetWeather(BEI_JING))
  }
}

整个流程用一张图来标明即:

Android:处理 MVI 架构实战痛点

改善版别 1:运用 DataBinding 防抖

考虑到 DataBinding ObservableField 存在防抖特性,故页面可通过 ObservableField 完结结束状态改变,尽可能消除 “控件刷新” 功用开销。

class MainPageActivity : Android_Activity(){
  private val model : MainPageModel
 private val views : MainPageViews
  fun onCreate(){
  lifecycleScope.launch {
  repeatOnLifecycle(Lifecycle.State.STARTED) {
   model.stateFlow.collect {uiStates ->
       views.progress.set(uiStates.isLoading)
       views.weatherInfo.set(uiStates.weather.info)
     ...
    }
  }
  model.input(Intent.GetWeather(BEI_JING))
  }
 class MainPageViews : Jetpack_ViewModel() {
    val progress = ObservableBoolean(false)
  val weatherInfo = ObservableField<String>("")
   ...
  }
}

不过这要求开发者具有 DataBinding 运用经历、额外书写 DataBinding 样板代码和 XML 绑定。

运用 distinctUntilChanged

除了 DataBinding,网上还说到有 2 类方案:

一类是通过 distinctUntilChanged 来为 ViewStates 的属性供应防抖,

但如此后续便难屏蔽 diff,只能露出给开发者手动 map distinct 分流,增加手写代码量和认知本钱,

class View-Controller : Android-Activity() {
 fun onCreate() {
  lifecycleScope.launch {
   repeatOnLifecycle(Lifecycle.State.STARTED) {
    viewModel.uiState
       .map { it.isDownload }
       .distinctUntilChanged()
       .collect { progress = it }
    viewModel.uiState
       .map { it.Setting }
       .distinctUntilChanged()
       .collect { btnChecked = it }
     ...
    }
   }
  }
}

运用 RecyclerView DiffUtils

另一类是通过 RecyclerView 编写页面。

如此便难支持杂乱交互作用、简单引进其他不可预期问题,也难在大都开发者中遍及开(有点为 MVI 而 MVI),且 DiffUtils 需手动配备,equals 列表鳞次栉比易漏写或写错,

val diff = object : DiffUtil.ItemCallback<ViewStates>() {
 override fun areItemsTheSame(oldItem: ViewStates, newItem: ViewStates): Boolean {
  return oldItem.equals(newItem)
  }
​
 override fun areContentsTheSame(oldItem: ViewStates, newItem: ViewStates): Boolean {
  return oldItem.progress().equals(newItem.progress())
   && ... equals ...
   && ... equals ...
   && ... equals ...
    ...
  }
}

易得 diff 办法皆存在学习本钱和运用本钱,当搭档写多了感觉厌烦,便自动回归原始,架构意图前功尽弃。

故我们只好另辟蹊径,探求少有人走的路,

改善版别 2:运用 Sealed Class 分流

因为不强迫开发者仅运用 “照应式编程”,故无缝隙需添补,乃至无需通过 data class 聚合 UiStates、也无线程安全问题,乃至无需 Actions 和 reduce,

每个页面可以简单通过 Intent 来包含入参和成果的传递,loading、error 等 Action 可以通过单独的 Intent 来反映,如此将 MVI 中最繁琐的 Action 规划拍平:

sealed class MainIntent {
  data class Loading(var progress: Boolean) : MainIntent()
  data class Info(var title: String) : MainIntent()
  ...
}
​
class Model : Jetpack-ViewModel() {
  private val _states = MutableLiveData<MainIntent>()
  val states = _states.asLiveData()
  fun request(intent: Intent){
    when(intent){
      is Intent.XXX -> {
        _states.setValue(MainIntent.Loading(true))
        _states.setValue(MainIntent.Info(DataRepository.getInfo()))
        _states.setValue(MainIntent.Loading(false))
      }
    }
  }
}
​
class View-Controller : Android-Activity() {
  private val model : Model
  private val holder : StateHolder
  fun onCreate(){
    model.states.observe(this){
     when(it){
        is MainIntent.Loading -> holder.progress = it.progress
        is MainIntent.Info -> holder.tvTitle = it.title
     ...
      }
    }
  }
}

可是 BehaviorSubject 天然不合适连续发送消息的场景,

例如息屏(页面生命周期脱离 STARTED)期间所获消息,BehaviorSubject 仅存留最后一个,那么分流规划下,亮屏后(页面生命周期重回 STARTED)多种类消息只会推送最后一个,其余皆丢掉(比如 Loading、Success、Error 等数据,毕竟只照应 Error),

故改用 PublishSubject,比如 SharedFlow 来处理。

改善版别 3:运用 SharedFlow 回推成果

SharedFlow 内有一行列,如欲亮屏后自动推送多种类消息,则可将 replay 次数设置为与行列长度一起,例如 10,

class Model : Jetpack-ViewModel() {
 private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
  MutableSharedFlow(
   onBufferOverflow = BufferOverflow.DROP_OLDEST,
   extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
   replay = DEFAULT_QUEUE_LENGTH
   )
  }
 val sharedFlow = _sharedFlow.asSharedFlow()
 companion object {
  private const val DEFAULT_QUEUE_LENGTH = 10
  }
}

因为 replay 会重走设定次数中行列的元素,故重走 STARTED 时会重走全部,包括已消费和未消费过,视觉上给人感觉即,控件上旧数据 “一闪而过”,

这体验并不好,

改善版别 4:通过计数避免重复回推

故此处可加个判别 —— 如已消费,则下次 replay 时不消费。

class Model : class Model : Jetpack-ViewModel() {
 private var observerCount = 0
 private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
  MutableSharedFlow(
   onBufferOverflow = BufferOverflow.DROP_OLDEST,
   extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
   replay = DEFAULT_QUEUE_LENGTH
   )
  }
 val sharedFlow = _sharedFlow.asSharedFlow()
 companion object {
  private const val DEFAULT_QUEUE_LENGTH = 10
  }
}
​
data class ConsumeOnceValue<E>(
 var consumeCount: Int = 0,
 val value: E
)
​
class View-Controller : Android-Activity() {
  private val model : Model
  private val holder : StateHolder
  fun onCreate(){
    lifecycleScope?.launch {
   repeatOnLifecycle(Lifecycle.State.STARTED) {
    model.states.collect {
     if (version > currentVersion) {
      if (model.consumeCount >= observerCount) return@collect
      model.consumeCount++
      when(it){
              is MainIntent.Download -> holder.progress = it.progress
              is MainIntent.Setting -> holder.btnChecked = it.btnChecked
              is MainIntent.Info -> holder.tvTitle = it.title
              is MainIntent.List -> holder.list = it.list
            }
      }
     }
    }
   }
  }
}

但每次创建一页面都需如此写一番,岂不伤心,

故可将其内聚,一致抽取至单独结构维护,

MVI-Dispatcher-KTX 应运而生,

改善版别 5:将样板逻辑内聚

如下,通过将 repeatOnLifecycle、计数比对、mutable/immutable 等样板逻辑内聚,

open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
 private var observerCount = 0
 private val _sharedFlow: MutableSharedFlow<ConsumeOnceValue<E>>? by lazy {
  MutableSharedFlow(
   onBufferOverflow = BufferOverflow.DROP_OLDEST,
   extraBufferCapacity = initQueueMaxLength(),
   replay = initQueueMaxLength()
   )
  }
​
 protected open fun initQueueMaxLength(): Int {
  return DEFAULT_QUEUE_LENGTH
  }
​
 fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
  observerCount++
  activity?.lifecycle?.addObserver(this)
  activity?.lifecycleScope?.launch {
   activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
    _sharedFlow?.collect {
     if (it.consumeCount >= observerCount) return@collect
     it.consumeCount++
     observer.invoke(it.value)
     }
    }
   }
  }
​
 override fun onDestroy(owner: LifecycleOwner) {
  super.onDestroy(owner)
  observerCount--
  }
​
 protected suspend fun sendResult(event: E) {
  _sharedFlow?.emit(ConsumeOnceValue(value = event))
  }
​
 fun input(event: E) {
  viewModelScope.launch { onHandle(event) }
  }
​
 protected open suspend fun onHandle(event: E) {}
​
 data class ConsumeOnceValue<E>(
  var consumeCount: Int = 0,
  val value: E
  )
​
 companion object {
  private const val DEFAULT_QUEUE_LENGTH = 10
  }
}

如此开发者哪怕不熟 MVI、mutable,只需注重 “input-output” 两处即可自动完结 “单向数据流” 开发,

Android:处理 MVI 架构实战痛点

改善版别 6:增加 version 避免订阅回推

为了改善 “副作用”(关于 “副作用” 见上期解析),通常是传输过程中兼并 UiStates 和 UiEvents,并在照应时分隔处理,这也和 “照应式编程” 串流规划不约而同。

对此官方做法是,将 UiEvents 整合到 UiStates,界面工作 | Android Developers

笔者以为,此做法相较于 UiStates 和 UiEvents 分隔发送的利益在于,使 UiEvents 同处于 STATRED 环节照应,避免手写遗失乃至引发 “弹窗无法获取 token” 等状况,

缺陷是,需求手动 filterNot 屏蔽已消费工作,增加学习本钱且埋下手写的一起性危险。

故笔者采用的是另一种办法 —— 将 UiState 整合到 UiEvent,照应时再将 UiState 和 UiEvent 解离。也即我们可以选用 PublishSubject 来做查询者,并在查询者回调中,单独对 UiState 采用 BehaviorSubject(比如 ObservableField)的办法来告诉控件照应和烘托。

故此处可再加个 verison 比对,

open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
 private var version = START_VERSION
 private var currentVersion = START_VERSION
 private var observerCount = 0
​
  ...
 fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
  currentVersion = version
  observerCount++
  activity?.lifecycle?.addObserver(this)
  activity?.lifecycleScope?.launch {
   activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
    _sharedFlow?.collect {
     if (version > currentVersion) {
      if (it.consumeCount >= observerCount) return@collect
      it.consumeCount++
      observer.invoke(it.value)
      }
     }
    }
   }
  }
​
 protected suspend fun sendResult(event: E) {
  version++
  _sharedFlow?.emit(ConsumeOnceValue(value = event))
  }
​
 companion object {
  private const val DEFAULT_QUEUE_LENGTH = 10
  private const val START_VERSION = -1
  }
}

如此即可从根源上消除 “照应式编程” 的缝隙,且不管团队成员是否了解 “照应式编程”,都可快速安稳迭代,不繁殖不可预期问题。

class MainPageActivity : Android_Activity(){
  private val model : MainPageModel
 private val views : MainPageViews
  fun onOutput(){
  model.output(this){ intent ->
   when(intent){
    MainIntent.Progress -> views.progress.set(intent.progress)
    MainIntent.Weather -> views.weatherInfo.set(intent.weather)
    MainIntent.Error -> showErrorDialog()
    }
   }
  model.input(Intent.GetWeather(BEI_JING))
  }
 class MainPageViews : Jetpack_ViewModel() {
    val progress = ObservableBoolean(false)
  val weatherInfo = ObservableField<String>("")
   ...
  }
}

与此同时该方式只是改善消息分发环节,结束仍然清晰差异有 State 和 Event,最大极限统筹学习本钱、功用和安稳。

注:SharedFlow 仅限于 Kotlin 项目,如 Java 项目也想用,可参阅 MVI-Dispatcher 规划,其内部维护一行列,通过依据 LiveData 改造的 Mutable-Result 亦满意完结上述功用。

综上

理论模型皆旨在特定环境下处理特定问题,直用于生产环境或存在不可预期问题,故我们不断检验、沟通和更新。

感谢脚踏实地检验反应沟通的小伙伴,让 MVI-Dispatcher 系结构得以演化至今。

Github:MVI-Dispatcher

Github:MVI-Dispatcher-KTX