V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
KunMinX
V2EX  ›  Android

repeatOnLifecycle + SharedFlow 隐藏坑排查与治理

  •  
  •   KunMinX · 2022-07-18 13:16:29 +08:00 · 8445 次点击
    这是一个创建于 852 天前的主题,其中的信息可能已经有所发展或是发生改变。

    上期《 Google Android 架构设计拆解及改善建议》侧重拆解官方架构 “领域层” 设计误区,并给出改善建议 —— 通过 MVI-Dispatcher 承担 Event-Handler ,

    然有小伙伴表示,不仅要 MVI-Dispatcher ,还想看看 Kotlin 版 MVI 实践

    故这期,我们肝个 MVI-Dispatcher-KTX 示例项目,相信查阅后你会耳目一新。

     

    收藏或置顶 顺滑转场 删除笔记

     

    项目简介

    “单向数据流” 是近年公认 “图形化客户端开发” 领域最佳实践,

    MVI-Dispatcher 通过内聚抹除 “单向数据流” 学习成本,使团队新手在不熟 mutable 、MVI 情况下,仅根据简明易懂 input-output 设计亦可自动实现 “单向数据流” 开发,

    MVI-Dispatcher-KTX 接口及特性与 MVI-Dispatcher 保持一致,可彻底消除 mutable 样板代码;可杜绝 setValue/emit 误用滥用页面中;且可无缝整合至 Jetpack MVVM 等模式项目。

    implementation 'com.kunminx.arch:mvi-dispatch-ktx:5.1.0-beta'
    

     

     

    What’s More

    本项目由 100% Java MVI-Dispatcher 项目改造而来,

    如您正在学习 Kotlin ,通过横向对比 MVI-Dispatcher 项目,可快速了解 Android Studio 一键转换后,为因地制宜遵循 Kotlin 特性 /风格 /思维,我们还可手动完成哪些调整修缮。

    区别于避重就轻实验性示例,MVI-Dispatcher 及 MVI-Dispatcher-KTX 提供完成一款记事本软件最少必要源码实现。

    故通过该示例您还可获得内容包括:

    1.整洁代码风格 & 标准命名规范

    2.对 “视图控制器” 深入理解 & 正确使用

    3.AndroidX 和 Material Design 全面使用

    4.ConstraintLayout 约束布局使用

    5.十六进制复合状态管理最佳实践

    6.优秀用户体验 & 交互设计

     

    这就,完了?

    不,封装测试过程中,还遇到点花絮。

     

    SharedFlow 也 “丢” 事件?

    同 MVI-Dispatcher ,MVI-Dispatcher-KTX 在 sample module 提供一系列常规 + 暴力测试,

    我们于 ComplexRequester 安排 4 组事件选项,事件 1 可轮询通知事件 4 回推 UI ,事件 2 延迟 200 毫秒后可回推 UI ,事件 3 可直接回推 UI ,

    class ComplexRequester : MviDispatcherKTX<ComplexEvent>() {
      override suspend fun onHandle(event: ComplexEvent) {
        when (event) {
          is ComplexEvent.ResultTest1 -> interval(100).collect { input(ComplexEvent.ResultTest4(it)) }
          is ComplexEvent.ResultTest2 -> timer(1000).collect { sendResult(event) }
          is ComplexEvent.ResultTest3 -> sendResult(event)
          is ComplexEvent.ResultTest4 -> sendResult(event)
        }
      }
      ...
    }
    

    与此同时,我们在 MainActivity 通过 output 函数注册观察 MVI-Dispatcher-KTX ,并通过 input 函数向 MVI-Dispatcher-KTX 发起事件 1 、2 、3 ,

    class MainActivity : BaseActivity() {
    
      ...
      
      override fun onOutput() {
        complexRequester.output(this) { complexEvent ->
          when (complexEvent) {
            is ComplexEvent.ResultTest1 -> Log.d("e", "---1")
            is ComplexEvent.ResultTest2 -> Log.d("e", "---2")
            is ComplexEvent.ResultTest3 -> Log.d("e", "---3")
            is ComplexEvent.ResultTest4 -> Log.d("e", "---4 " + complexEvent.count)
          }
        }
      }
      
      override fun onInput() {
        super.onInput()
        
        complexRequester.input(ComplexEvent.ResultTest1())
        
        complexRequester.input(ComplexEvent.ResultTest2())
        complexRequester.input(ComplexEvent.ResultTest2())
        complexRequester.input(ComplexEvent.ResultTest2())
        complexRequester.input(ComplexEvent.ResultTest2())
        
        complexRequester.input(ComplexEvent.ResultTest3())
        complexRequester.input(ComplexEvent.ResultTest3())
        complexRequester.input(ComplexEvent.ResultTest3())
      }
    }
    

    结果出人意料:

    com.kunminx.purenote_ktx D/e: ---4 0
    com.kunminx.purenote_ktx D/e: ---4 1
    com.kunminx.purenote_ktx D/e: ---4 2
    com.kunminx.purenote_ktx D/e: ---4 3
    com.kunminx.purenote_ktx D/e: ---4 4
    com.kunminx.purenote_ktx D/e: ---4 5
    com.kunminx.purenote_ktx D/e: ---4 6
    com.kunminx.purenote_ktx D/e: ---4 7
    com.kunminx.purenote_ktx D/e: ---2
    com.kunminx.purenote_ktx D/e: ---2
    com.kunminx.purenote_ktx D/e: ---2
    com.kunminx.purenote_ktx D/e: ---2
    com.kunminx.purenote_ktx D/e: ---4 8
    com.kunminx.purenote_ktx D/e: ---4 9
    com.kunminx.purenote_ktx D/e: ---4 10
    com.kunminx.purenote_ktx D/e: ---4 11
    com.kunminx.purenote_ktx D/e: ---4 12
    

    事件 3 回推结果呢?

    MVI-Dispatcher 测试时无此问题,为何 MVI-Dispatcher-KTX 便有?

    于是继续打 Log 观察:

    class ComplexRequester : MviDispatcherKTX<ComplexEvent>() {
      override suspend fun onHandle(event: ComplexEvent) {
        when (event) {
          ...
          is ComplexEvent.ResultTest3 -> {
            Log.d("---", "ResultTest3-sendResult")
            sendResult(event)
          }
        }
      }
    }
    

    MVI-Dispatcher-KTX 基类中观察 SharedFlow 收集时机:

    open class MviDispatcherKTX<E> : ViewModel() {
      fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
        initQueue()
        activity?.lifecycleScope?.launch {
          Log.d("---", "activity.lifecycleScope.launch")
          activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
            Log.d("---", "activity.repeatOnLifecycle")
            _sharedFlow?.collect { observer.invoke(it) }
          }
        }
      }
      ...
    }
    

    继续输出结果:

    com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
    com.kunminx.purenote_ktx D/e: ---4 0
    com.kunminx.purenote_ktx D/e: ---4 1
    com.kunminx.purenote_ktx D/e: ---4 2
    com.kunminx.purenote_ktx D/e: ---4 3
    com.kunminx.purenote_ktx D/e: ---4 4
    

    发现端倪 —— sharedFlow.emit 事件 3 时机早于 activity.repeatOnLifecycle 时机,错过 sharedFlow 收集时,

    故此处将 sharedFlow replay 值改为 1 验证下:

    open class MviDispatcherKTX<E> : ViewModel() {
      private var _sharedFlow: MutableSharedFlow<E>? = null
    
      private fun initQueue() {
        if (_sharedFlow == null) _sharedFlow = MutableSharedFlow(
          replay = 1,
          onBufferOverflow = BufferOverflow.DROP_OLDEST,
          extraBufferCapacity = initQueueMaxLength()
        )
      }
      ...
    }
    

    这下收到,确实是时机问题,也即 sharedFlow 并非人眼感知到的 “丢事件”,而是其默认 replay = 0 ,不自动回推缓存数据给订阅者,该设计符合 “事件” 场景,

    且此处如设置为 replay = 1 ,发射 3 次也仅能收到 1 次,故修改 replay 方案 pass 。

    com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
    com.kunminx.purenote_ktx D/e: ---3
    com.kunminx.purenote_ktx D/e: ---4 0
    com.kunminx.purenote_ktx D/e: ---4 1
    com.kunminx.purenote_ktx D/e: ---4 2
    com.kunminx.purenote_ktx D/e: ---4 3
    com.kunminx.purenote_ktx D/e: ---4 4
    

    那怎办,repeatOnLifecycle STARTED 回调相对 emit 存在时延,其实在 Activity 中易解决,即通过 View.post 时机,让 emit 处于 MessageQueue 中顺序执行,如此便能确保时机正确,

    但发射一事件还要 View.post ,显然易忘记、造成一致性问题,且 MVI-Dispatcher-KTX 采用内聚设计,故此处不妨往 input 方法注入 Activity ,再于内部拿取 decorView 自动完成 post …

    open class MviDispatcherKTX<E> : ViewModel() {
      ...
      fun input(event: E, activity: AppCompatActivity?) {
        activity?.window?.decorView?.post {
          viewModelScope.launch { onHandle(event) }
        }
      }
    }
    

    倒也行,不过每次 input 都额外注入个 Activity ,这写法是不有点莫名其妙?

    且如我想在 MVI-Dispatcher-KTX 子类内部 input “side effect” 怎办?故该方案暂且 pass 。

    … 还有无别的办法?

    有,

    考虑到 “错过事件” 情形较极端,常规 “从数据层取数据” 等操作,由于操作有其耗时,不易遇见;

    如是页面 onCreate 环节末尾发送某 sealed.object 事件,由于毫不费时,则易先于 activity.repeatOnLifecycle(Lifecycle.State.STARTED),错过时机,

    故此处可于每次 input 时自动延迟 1 毫秒 ——

    默认设置为 1 毫秒,且通过维护一 delayMap 自动判断时机取消延迟:

    open class MviDispatcherKTX<E> : ViewModel() {
    
      ...
      
      fun input(event: E) {
        viewModelScope.launch {
          if (needDelayForLifecycleState) delayForLifecycleState().collect { onHandle(event) }
          else onHandle(event)
        }
      }
    
      private val needDelayForLifecycleState
        get() = delayMap.isNotEmpty()
    }
    

    输出 Log 看看:

    com.kunminx.purenote_ktx D/---: activity.lifecycleScope.launch
    com.kunminx.purenote_ktx D/---: activity.repeatOnLifecycle
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/e: ---3
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/e: ---3
    com.kunminx.purenote_ktx D/---: ResultTest3-sendResult
    com.kunminx.purenote_ktx D/e: ---3
    com.kunminx.purenote_ktx D/e: ---4 0
    com.kunminx.purenote_ktx D/e: ---4 1
    com.kunminx.purenote_ktx D/e: ---4 2
    com.kunminx.purenote_ktx D/e: ---4 3
    com.kunminx.purenote_ktx D/e: ---4 4
    

    至此,3 个事件 3 皆收到。

     

    UnPeek-LiveData 如何?

    让我们借一部说话,看看 UnPeek-LiveData 表现:

    public class MainActivity extends BaseActivity {
      private UnPeekLiveData<String> unpeek = new UnPeekLiveData<>();
      
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        ...
    
        unpeek.observe(this,s -> {
          Log.d("----",s);
        });
        unpeek.setValue("hahaha");
      }
    }
    

    输出 Log 看看:

    com.kunminx.unpeeklivedata D/----: hahaha
    

    repeatOnLifecycle + sharedFlow 扬言替代 LiveData 于事件场景,结果其自身也属半成品 ...

    鉴于 UnPeek-LiveData 表现,加之 MVI-Dispatcher 内部维护定长队列,故默认无原生 LiveData “连发丢事件” 问题。

     

    最后

    MVI-Dispatcher-KTX + 排坑小故事至此已分享完毕,目前 MVI-Dispatcher 及 MVI-Dispatcher-KTX 正在公测中,欢迎各位大佬测试反馈:

    Github:MVI-Dispatcher:

    https://github.com/KunMinX/MVI-Dispatcher

    Github:MVI-Dispatcher-KTX:

    https://github.com/KunMinX/MVI-Dispatcher-KTX

    5 条回复    2022-08-14 01:02:39 +08:00
    kyleLin
        1
    kyleLin  
       2022-07-19 00:56:58 +08:00   ❤️ 6
    个人觉得你没搞懂为什么 Andriod 文档里的对于 flow 的使用加上了 repeatOnLifecycle 。

    其次你写的 UnPeek-LiveData 之所以没有丢事件不是很正常的事情吗?一方面你知道 sharedFlow reply 是 0 ,一方面你又要拿一个 Livedata 来做比较,前者是因为 collect 的时候生命周期不符合要求,后者能重新发射事件是因为 version 的判断导致没消耗的事件能够在生命周期符合要求的时候发射了出去。

    如果你是为了推荐你自己写的库大可不必,至少你在踩 flow 的时候得讲清楚你不推荐的原因和讲清楚你做对比的例子的本质。不然只会让不了解的人产生误解,然后往歪路越走越远。
    KunMinX
        2
    KunMinX  
    OP
       2022-07-19 11:19:22 +08:00
    可以诋毁,可以凭个人主观情感故意曲解,但不可以漠视。MVI-Dispatcher 系列分享到此为止,就让子弹飞一会。
    kyleLin
        3
    kyleLin  
       2022-07-19 17:27:46 +08:00
    @KunMinX 你可能有点被害妄想症,我的发言里哪一句是诋毁或者曲解?
    fromzero
        4
    fromzero  
       2022-07-19 19:28:24 +08:00 via Android
    你的 ui 基于事件驱动,当然不可避免会有粘性事件的场景。跟人家 flow 和 repeatLifeCycle ,甚至 livedata 没有半毛关系,人家本来就不是为这种场景设计的。
    贬低 Android 官方,抬高自己?显得你很牛逼?
    这种延迟 1ms 的解决方案看起来也很奇葩,感觉你对这个粘性事件着魔了一样,围绕着大作文章。
    GetCore
        5
    GetCore  
       2022-08-14 01:02:39 +08:00
    无意义的争论,楼主不该发这里。

    /go/flamewar
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2852 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 07:43 · PVG 15:43 · LAX 23:43 · JFK 02:43
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.