您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

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

时间:2022-08-23 17:06:27  来源:掘金  作者:KunMinX

通过本文可快速了解:

1.为何使用 MVI

2.为何最终考虑 SharedFlow 实现

3.repeatOnLifecycle + SharedFlow 实现 MVI 思路

为何使用 MVI

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

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

  • 所获 States 总是最新且来源可靠唯一
  • 可消除 mutable 样板代码

“消除样板代码” 相信开发者深有体会。“所获 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
来源:稀土掘金



Tags:Android   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Android Emulator黑屏怎么办 Android模拟器黑屏解决方法
Android Emulator黑屏问题困扰了非常多的玩家,Android Emulator作为一款安卓模拟器,可以让你在电脑上运行和浏览安卓应用程序,但是程序本身不是很稳定,很容易会出现黑屏,启动不了...【详细内容】
2024-03-04  Search: Android  点击:(37)  评论:(0)  加入收藏
Android 谷歌三件套:解锁谷歌生态!
大家是不是遇到这个情况?当我们需要下载一些国外的游戏或者软件的时候,需要在手机里面安装Google Play商店,然后通过Google Play商店下载国外软件!为了帮助大家使用上各种好用的...【详细内容】
2024-01-02  Search: Android  点击:(113)  评论:(0)  加入收藏
Android开发中常见的Hook技术有哪些?
Hook技术介绍Hook技术是一种在软件开发中常见的技术,它允许开发者在特定的事件发生时插入自定义的代码逻辑。常见的应用场景包括在函数调用前后执行特定的操作,或者在特定的事...【详细内容】
2023-12-25  Search: Android  点击:(86)  评论:(0)  加入收藏
在Android应用开发中使用NFC功能
NFC介绍NFC是指“近场通讯”(Near Field Communication),它是一种短距离无线通信技术,允许设备在非接触或极短距离内进行通信。NFC通常用于移动支付、门禁系统、智能标签和其他...【详细内容】
2023-12-22  Search: Android  点击:(102)  评论:(0)  加入收藏
关于Android图像Bitmap类,你要知道的一切
Bitmap介绍Bitmap是一种图像文件格式,它由像素阵列组成,每个像素都有自己的颜色信息。在计算机图形学中,Bitmap图像可以被描述为一个二维的矩阵,其中每个元素代表一个像素的颜色...【详细内容】
2023-12-19  Search: Android  点击:(99)  评论:(0)  加入收藏
Android开发中如何进行单元测试?
单元测试介绍单元测试是软件开发中的一种测试方法,用于验证代码中的最小可测试单元(通常是函数或方法)是否按预期工作。单元测试通常由开发人员编写,旨在隔离和测试代码的特定部...【详细内容】
2023-12-11  Search: Android  点击:(168)  评论:(0)  加入收藏
我的手机我做主,如何为Android手机应用换图标?
作为一名Android用户,你是否曾经为自己的手机桌面感到单调而乏味?虽然Android系统的桌面定制性已经非常强大,但有时候我们还是希望能够在细节上做出一些改变,尤其是对于那些每天...【详细内容】
2023-12-10  Search: Android  点击:(61)  评论:(0)  加入收藏
了解Android系统架构中的HAL硬件抽象层
在Android系统中,HAL的存在使得不同厂商的硬件可以统一被上层的应用程序调用,从而提高了系统的兼容性和可移植性。HAL还可以帮助开发者更方便地开发应用程序,因为他们不需要为...【详细内容】
2023-12-06  Search: Android  点击:(203)  评论:(0)  加入收藏
谷歌 CEO 皮查伊建议 Android 用户不要侧载应用,称非常危险
iOS和Android的一个显著差异是,Android支持用户从第三方渠道安装应用程序(即“侧载”)。然而,谷歌似乎并不希望用户这样做。最近,在与Epic Store的法律诉讼中,谷歌首席执行官桑达...【详细内容】
2023-11-20  Search: Android  点击:(167)  评论:(0)  加入收藏
Android数据对象序列化原理与应用
序列化与反序列化「序列化」是将对象转换为可以存储或传输的格式的过程。在计算机科学中,对象通常是指内存中的数据结构,如数组、列表、字典等。通过序列化,可以将这些对象转换...【详细内容】
2023-11-14  Search: Android  点击:(273)  评论:(0)  加入收藏
▌简易百科推荐
Android Emulator黑屏怎么办 Android模拟器黑屏解决方法
Android Emulator黑屏问题困扰了非常多的玩家,Android Emulator作为一款安卓模拟器,可以让你在电脑上运行和浏览安卓应用程序,但是程序本身不是很稳定,很容易会出现黑屏,启动不了...【详细内容】
2024-03-04  18183游戏网    Tags:Android Emulator   点击:(37)  评论:(0)  加入收藏
Android开发中常见的Hook技术有哪些?
Hook技术介绍Hook技术是一种在软件开发中常见的技术,它允许开发者在特定的事件发生时插入自定义的代码逻辑。常见的应用场景包括在函数调用前后执行特定的操作,或者在特定的事...【详细内容】
2023-12-25  沐雨花飞蝶  微信公众号  Tags:Android   点击:(86)  评论:(0)  加入收藏
在Android应用开发中使用NFC功能
NFC介绍NFC是指“近场通讯”(Near Field Communication),它是一种短距离无线通信技术,允许设备在非接触或极短距离内进行通信。NFC通常用于移动支付、门禁系统、智能标签和其他...【详细内容】
2023-12-22  沐雨花飞蝶  微信公众号  Tags:Android   点击:(102)  评论:(0)  加入收藏
关于Android图像Bitmap类,你要知道的一切
Bitmap介绍Bitmap是一种图像文件格式,它由像素阵列组成,每个像素都有自己的颜色信息。在计算机图形学中,Bitmap图像可以被描述为一个二维的矩阵,其中每个元素代表一个像素的颜色...【详细内容】
2023-12-19  沐雨花飞蝶  微信公众号  Tags:Android   点击:(99)  评论:(0)  加入收藏
Android开发中如何进行单元测试?
单元测试介绍单元测试是软件开发中的一种测试方法,用于验证代码中的最小可测试单元(通常是函数或方法)是否按预期工作。单元测试通常由开发人员编写,旨在隔离和测试代码的特定部...【详细内容】
2023-12-11  沐雨花飞蝶  微信公众号  Tags:Android   点击:(168)  评论:(0)  加入收藏
一篇聊聊Jetpack Room实现数据存储持久性
Room介绍Room 是 Android Jetpack 组件库中的一部分,它是用于在 Android 应用中进行本地数据库访问和管理的库。Room 提供了一个抽象层,使开发者能够更轻松地访问 SQLite 数据...【详细内容】
2023-12-08  沐雨花飞蝶  微信公众号  Tags:Jetpack   点击:(143)  评论:(0)  加入收藏
了解Android系统架构中的HAL硬件抽象层
在Android系统中,HAL的存在使得不同厂商的硬件可以统一被上层的应用程序调用,从而提高了系统的兼容性和可移植性。HAL还可以帮助开发者更方便地开发应用程序,因为他们不需要为...【详细内容】
2023-12-06  沐雨花飞蝶  微信公众号  Tags:Android   点击:(203)  评论:(0)  加入收藏
我们一起聊聊 IntentService 与 Service 的区别?
Service介绍Service组件是Android应用开发中的四大组件之一,用于在后台执行长时间运行的操作或处理远程请求。它可以在没有用户界面的情况下执行任务,并且可以与其他应用组件...【详细内容】
2023-12-06  沐雨花飞蝶  微信公众号  Tags:IntentService   点击:(171)  评论:(0)  加入收藏
Android数据对象序列化原理与应用
序列化与反序列化「序列化」是将对象转换为可以存储或传输的格式的过程。在计算机科学中,对象通常是指内存中的数据结构,如数组、列表、字典等。通过序列化,可以将这些对象转换...【详细内容】
2023-11-14  沐雨花飞蝶  微信公众号  Tags:Android   点击:(273)  评论:(0)  加入收藏
你了解Android中的SELinux吗?
SELinux介绍SELinux(Security-Enhanced Linux)是一种安全增强的Linux操作系统,它通过强制访问控制(MAC)机制来提供更高级别的系统安全保护。相比于传统的Linux访问控制机制(DAC),SEL...【详细内容】
2023-11-09  沐雨花飞蝶  微信公众号  Tags:Android   点击:(265)  评论:(0)  加入收藏
站内最新
站内热门
站内头条