<返回更多

Android实战:解决 MVI 架构实战痛点

2022-08-23  掘金  KunMinX
加入收藏

通过本文可快速了解:

1.为何使用 MVI

2.为何最终考虑 SharedFlow 实现

3.repeatOnLifecycle + SharedFlow 实现 MVI 思路

为何使用 MVI

MVI 是一响应式模型,通过唯一入口入参,并从唯一出口接收结果和完成响应。

换言之,通过将 States 聚合于 MVI-Model,页面根据回传结果统一完成 UI 渲染,可确保

“消除样板代码” 相信开发者深有体会。“所获 States 总是最新且来源可靠唯一”,对此存疑,故我们继续一探究竟。

MVI 原始理论模型

根据网传 MVI 理论模型,经典 MVI 模型伪代码示例如下:

data class ViewStates(
  val progress: Int,
  val btnChecked: Boolean,
  val title: String,
  val list: List<User>,
)

class Model : Jetpack-ViewModel() {
  private val _states = MutableLiveData<ViewStates>()
  val states = _states.asLiveData()
  fun request(intent: Intent){
    when(intent){
      is Intent.XXX -> {
        DataRepository.xxx.onCallback{
          val s = _states.getValue()
          s.progress = it.progress
          _states.setValue(s)
        }
      }
    }
  }
}
​
class View-Controller : Android-Activity() {
  private val binding : ViewBinding 
  private val model : Model
  fun onCreate(){
    model.states.observe(this){
      binding.progress = it.progress
      binding.btnChecked = it.btnChecked
      binding.tvTitle = it.title
      binding.rv.adapter.refresh(it.list)
    }
  }
}

易得经典 MVI 模型 “牵一发动全身”,也即无论为哪个控件修改状态,所有控件皆需重刷一遍状态,

如此在 Android View 系统下存在额外性能开销,当页面控件展示逻辑复杂,或需频繁刷新时,易产生掉帧现象,

改善版本 1:使用 DataBinding

考虑到 DataBinding ObservableField 存在防抖特性,故页面可考虑 ObservableField 完成末端状态改变,尽可能消除 “控件刷新” 性能开销。

class StateHolder : Jetpack-ViewModel() {
  val progress : ObservableField<Integer>()
  val btnChecked : ObservableField<Boolean>()
  val title : ObservableField<String>()
  val list : ObservableArrayList<User>()
}
​
class View-Controller : Android-Activity() {
  private val model : Model
  private val holder : StateHolder
  fun onCreate(){
    model.states.observe(this){
      holder.progress = it.progress
      holder.btnChecked = it.btnChecked
      holder.tvTitle = it.title
      holder.list = it.list
    }
  }
}

不过,以上只是免除末端控件刷新,Observe 回调中逻辑该走还是得走,

且需开发者具备 DataBinding 使用经验、额外书写 DataBinding 样板代码和 XML 绑定,

改善版本 2:使用 Sealed Class 分流

根据业务场景,将原本置于 data class 状态分流:

sealed class ViewStates {
  data class Download(var progress: Int) : ViewStates()
  data class Setting(var btnChecked: Boolean) : ViewStates()
  data class Info(var title: String) : ViewStates()
  data class List(var list: List<User>) : ViewStates()
}
​
class Model : Jetpack-ViewModel() {
  private val _states = MutableLiveData<ViewStates>()
  val states = _states.asLiveData()
  fun request(intent: Intent){
    when(intent){
      is Intent.XXX -> DataRepository.xxx.onCallback(_states::setValue)
    }
  }
}

如此可只走本次业务场景 UI 逻辑:

class View-Controller : Android-Activity() {
  private val model : Model
  private val holder : StateHolder
  fun onCreate(){
    model.states.observe(this){
      when(it){
        is ViewStates.Download -> holder.progress = it.progress
        is ViewStates.Setting -> holder.btnChecked = it.btnChecked
        is ViewStates.Info -> holder.tvTitle = it.title
        is ViewStates.List -> holder.list = it.list
      }
    }
  }
}

网上流行示例,包括官方示例,多探索和分享至此。

然实战中易得,BehaviorSubject、LiveData、StateFlow 等 replay 1 模型皆理想化 “过度设计” 产物,在生产环境中易滋生不可预期问题,

例如息屏(页面生命周期离开 STARTED)期间所获消息,replay 1 模型仅存留最后一个,那么 MVI 分流设计下,亮屏后(页面生命周期重回 STARTED)多种类消息只会推送最后一个,其余皆丢失,

改善版本 3:使用 SharedFlow 回推结果

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

class Model : class Model : Jetpack-ViewModel() {
  private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
    MutableSharedFlow(
      onBufferOverflow = BufferOverflow.DROP_OLDEST,
      extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
      replay = DEFAULT_QUEUE_LENGTH
    )
  }
  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
    )
  }

  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 ViewStates.Download -> holder.progress = it.progress
              is ViewStates.Setting -> holder.btnChecked = it.btnChecked
              is ViewStates.Info -> holder.tvTitle = it.title
              is ViewStates.List -> holder.list = it.list
            }
          }
        }
      }
    }
  }
}

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

故可将其内聚,统一抽取至单独框架维护,

MVI-Dispatcher-KTX 应运而生,

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

如下,通过将 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)
        }
      }
    }
  }
​
  fun output(fragment: Fragment?, observer: (E) -> Unit) {
    observerCount++
    fragment?.viewLifecycleOwner?.lifecycle?.addObserver(this)
    fragment?.viewLifecycleOwner?.lifecycleScope?.launch {
      fragment.viewLifecycleOwner.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” 两处即可自动完成 “单向数据流” 开发,

class View-Controller : Android-Activity() {
  private val model: MVI-Dispatcher
  fun onOutput(){
    model.output(this){
      when(it){
        is Intent.Download -> holder.progress = it.progress
        is Intent.Setting -> holder.btnChecked = it.btnChecked
        is Intent.Info -> holder.tvTitle = it.title
        is Intent.List -> holder.list = it.list
      }
    }
  }
  fun onInput(){
    model.input(Intent.Download)
  }
}

改善版本 6:添加 version 防止订阅回推

前不久在 Android 开发者公众号偶遇《Jetpack MVVM 发送 Events》,文中关于 “消费且只消费一次” 描述,感觉很贴切。

且经海量样本分析易知,敏捷开发过程中,实际高频存在问题即 “消息分发一致性问题”,与其刻意区分 State 和 Event 理论概念,不如二者合而为一,升级为简明易懂 “消费且只消费一次” 线上模型。

故此处可再加个 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
  }
}

如此便可实现 “多观察者消费且只消费一次”,解决页面初始化或息屏亮屏场景下 “Flow 错过收集” 且不滋生预期外错误:

对于 UI Event,例如通知前台弹窗、弹 Toast、页面跳转,可用该模型,

对于 UI State,例如 progress 更新,btnChecked 更新,亦可用该模型,

State 可通过 DataBinding ObservaField 或 Jetpack Compose mutableState 充当和响应,并托管于 Jetpack ViewModel,整个过程如下:

    表现层              领域层              数据层
unified Event  -> Domain Dispatcher -> Data Component
UI State/Event <- Domain Dispatcher <- Data Component

如此当页面旋屏重建时,页面自动从 Jetpack ViewModel 获取 ObservaField/mutableState 绑定和渲染控件,无需 replay 1 模型回推。

SharedFlow 仅限于 Kotlin 项目,如 JAVA 项目也想用,可参考 MVI-Dispatcher 设计,其内部维护一队列,通过基于 LiveData 改造的 Mutable-Result 亦圆满实现上述功能。

综上

理论模型皆旨在特定环境下解决特定问题,MVI 是一理想化理论模型,直用于生产环境或滋生不可预期问题,故我们不断尝试、交流、反馈和更新。

作者:KunMinX
链接:
https://juejin.cn/post/7134594010642907149
来源:稀土掘金

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>