Kotlin Flow上手指南(三) ShardFlow与StateFlow

8,067 阅读21分钟

前面几篇已经介绍了Flow的一些基本用法,及其背后的Channel

这是Kotlin协程系列的第四篇文章。

本篇将继续尝试以RxJava使用者的角度,探索Flow中更多进阶功能,以满足更多的使用场景。

Kotlin协程系列相关文章导航

扒一扒Kotlin协程

Kotlin Flow上手指南(一)基础应用

Kotlin Flow上手指南(二)ChannelFlow

Kotlin Flow上手指南(三)SharedFlow与StateFlow (本篇)

Kotlin版本 : 1.5.31

Coroutine 版本 : 1.5.2

以下正文。


SharedFlow

熟悉RxJava的人应该对其中的Subject有所印象,与于Observale这类需要调用subscribe订阅后才发送数据的数据流不同。

Subject热流,并且同时具有发送数据与接收数据的功能,既是生产者也是消费者。

而在Flow中,同样也有对应功能实现——SharedFlow

相比普通FlowSharedFlow意为可共享的数据流。

  • 对于同一个数据流,可以允许有多个订阅者共享
  • 不调用collect收集数据,也会开始发送数据。
  • 允许缓存历史数据
  • 发送数据函数都是线程安全的。

SharedFlow定义.png

SharedFlow本身的定义很简单,只比Flow多个历史数据缓存的集合,只允许订阅数据

就如同Kotlin中ListMutableList,一个仅表示可读,一个表示可读可写的类型。

ShardFlow同样也有一个可变类型的版本MutableShardFlow定义了发送数据功能

MutableSharedFlow定义.png

创建

首先来看MutableSharedFlow是如何创建的。

MutableSharedFlow(2).png

  • replay

    表示历史元素缓存区容量

    • 能够将最新的数据缓存到集合内,当历史缓存区满后,会移除最早的元素
    • 当在新消费者订阅了该数据流,会先将历史缓存区元素依次发送给新的消费者,然后才发送新元素。
  • extraBufferCapacity

    表示除历史缓存区外的额外缓存区容量,用于扩充内部整体缓存容量。

  • onBufferOverflow

    缓存区背压策略,默认是熟悉的BufferOverflow.SUSPEND,当额外缓冲区满后,挂起emit函数,暂停发送数据。

    只有在replayextraBufferCapacity均不为0时才支持其他背压策略。

简单使用

不同于FlowChannelFlow中需要利用FlowCollectorProducerScope来发送数据,由于MutableSharedFlow本身就拥有发送数据功能,使其用法更接近于日常使用MutableList

 fun test() = runBlocking{
     val sharedFlow = MutableSharedFlow<String>()
     
     //假设处于另一个类,异步发送数据
     val produce = launch(Dispatchers.IO){
         for (i in 0..50){
             sharedFlow.emit("data$i")
             delay(50)
         }
     }
 }

虽然此时并没有消费者订阅,但依旧会执行发送数据操作,只是目前没有设置历史缓存,所有数据都被"抛弃"了。

通常我们并不希望在消费者订阅端能够发送数据,只允许外部进行数据流订阅,此时就需要调用asSharedFlow函数,将可变的MutableSharedFlow转化为只读的SharedFlow

 fun test() = runBlocking{
     ...
      //模拟在外部调用
     val readOnlySharedFlow = sharedFlow.asSharedFlow()
     val scope = CoroutineScope(SupervisorJob())
     
     delay(100)
     val job1 = scope.launch { //消费者单独一个协程
         readOnlySharedFlow.map {
             delay(100)
             "$it receive 1"
         }
         .collect {
             println("collect1 result : $it")
         }
     }
     
     delay(1000)
     job1.cancel() //注意要关闭消费者所在的协程
 }
 ​
 collect1 result : data3 receive 1
 collect1 result : data4 receive 1
 collect1 result : data5 receive 1
 collect1 result : data6 receive 1
 collect1 result : data7 receive 1
 collect1 result : data8 receive 1
 collect1 result : data9 receive 1
 collect1 result : data10 receive 1
 collect1 result : data11 receive 1

我们模拟一个外部消费者,这里延迟100ms再订阅数据,就只接收到从订阅后开始发送的所有后续数据。

SharedFlow作为Flow的子类,自然也能够使用Flow的中间操作符。

这等效于RxJava中的PublishSubject

如果在SharedFlow创建时设置replay属性,比如设置为2,就会缓存最新的两个值,此时运行结果就变成了:

 val sharedFlow = MutableSharedFlow<String>(replay = 2)
 
 collect1 result : data1 receive 1
 collect1 result : data2 receive 1
 collect1 result : data3 receive 1
 collect1 result : data4 receive 1
 collect1 result : data5 receive 1
 collect1 result : data6 receive 1
 collect1 result : data7 receive 1
 collect1 result : data8 receive 1

原本在订阅前发送的两个值也被消费者收集到了,等效于RxJavaReplayRelay.createWithSize<String>(2)

SharedFlow热流,而collect是个挂起函数,会一直等待上游数据,不论上游是否发送数据。

所以对于SharedFlow需要注意消费者所在的协程内,后续任务是不会执行的

 fun test() = runBlocking{
     ...
     delay(200)
     val job2 = scope.launch { //消费者单独一个协程
         readOnlySharedFlow.map {"$it receive 2"}
         .collect{println("collect2 result : $it")}
     }
     
     delay(1000)
     job1.cancel()
     job2.cancel()
 }

如果再新增一个消费者,其就会继续接收上游新发送的数据,直到消费者所在协程被关闭。

 collect1 result : data3 receive 1
 collect2 result : data5 receive 2
 collect1 result : data4 receive 1
 collect2 result : data6 receive 2
 ...
 collect2 result : data13 receive 2
 collect1 result : data12 receive 1
 collect2 result : data14 receive 2
 collect1 result : data13 receive 1

但如果在job2的消费者中主动抛出异常:

 readOnlySharedFlow.map {
     if (it == "data6") throw Exception("test Exception")
     "$it receive 2"
 }
 ​
 collect1 result : data3 receive 1
 collect2 result : data5 receive 2
 collect1 result : data4 receive 1
 ​
 test Exception
 java.lang.Exception: test Exception
 ...

在消费者中出现了未捕获异常,此时根据消费者运行所在协程的Job类型有两种情况

  • 如果协程作用域(父协程)的context是Job,则抛出异常无法捕获,如果像是测试程序中使用runBlocking会直接抛出异常,程序崩溃。
  • 如果协程作用域(父协程)的context是SupervisorJob,则只会影响到消费者所在的协程,其他消费者接收数据不受影响。

所有消费者抛出的异常并不会影响上游共享数据流,当然所有SharedFlow的订阅者最好都利用catch操作符捕获住异常,也可在协程内设置CoroutineExceptionHandler进行捕获。

冷流转热流

RxJava中,允许将Subject的热流通过toFlowable函数转化为Flowable类型的冷流,但反过来将冷流转化为热流的功能,却似乎并没有提供。

2021-11-27更新:

感谢评论区大佬的指正补充,RxJava中提供了操作符publish冷流转化为热流

同时提供refCount将其转化为自动管理结束的热流,以及将publishrefCount合并的share操作符。

详情参见:关于 Observable 的冷热,常见的封装方式以及误区

而在ShardFlow中也同样提供了冷流转热流的函数——shareIn

shareIn代码块.png

这里started表示新创建的共享数据流的启动与停止策略

  • Eagerly

    立即开始发送数据源。并且消费端永远收集数据,只会收到历史缓存和后续新数据,直到所在协程取消

  • Lazily

    等待出现第一个消费者订阅后,才开始发送数据源。保证第一个消费者能收到所有数据,但后续消费者只能收到历史缓存和后续数据。

    消费者会永远等待收集数据,直到所在协程取消

  • WhileSubscribed

    可以说是Lazily策略的进阶版,同样是等待第一个消费者订阅后,才开始发送数据源。

    但其可以配置在最后一个订阅者关闭后,共享数据流上游停止的时间(默认为立即停止),与历史数据缓存清空时间(默认为永远保留)。

     public fun WhileSubscribed(
         stopTimeoutMillis: Long = 0, //上游数据流延迟结束,ms
         replayExpirationMillis: Long = Long.MAX_VALUE //缓冲数据清空延迟,ms
     ): SharingStarted
    

利用shareIn以及后文介绍的stateIn,即可将消耗一次资源从数据源获取数据的Flow数据流,转化为SharedFlowStateFlow,实现一对多的事件分发,并减少多次调用资源的损耗。

需要注意,在使用shareIn每次都会创建一个新SharedFlow实例,并且该实例会一直保留在内存中,直到被垃圾回收。

所以最好减少转换流的执行次数,不要在函数内每次都调用这类函数。

更多关于SharingStarted的使用场景,可以参见shareIn 和 stateIn 使用须知

SharedFlow原理分析

上一篇提到的ChannelFlow内部是通过Channel实现线程安全的多协程通信,那么SharedFlow的内部实现又是怎么样的呢?通过MutableSharedFlow工厂函数创建的SharedFlow,内部实际是创建了SharedFlowImpl对象,是使用数组缓存所有数据

SharedFlowImpl源码解析(1).png

发送数据

SharedFlowImpl这个类第一次看起来有点多,这里先从emittryEmit作为切入点,看看其是如何实现发送数据。

SharedFlowImpl源码解析emit.png

tryEmit内部通过synchronized加锁,是线程安全的。

而当快速通道tryEmit方法,返回false,也即是校验当前缓存数组已经满了,就会调用emitSuspend方法创建挂起函数

  1. 再一次加锁检测是否能够发送数据(校验缓存池是否已满),如果此时已经能够发送数据,则直接恢复挂起函数运行
  2. 如果不能发送数据,则将数据包装为Emitter类,并调用enqueueLocked来将Emitter类实例保存到数组内部。
  3. 如果已能够发送数据,或者额外缓冲区,会调用findSlotsToResumeLocked获取所有正在挂起等待的接收器挂起函数,然后遍历进行挂起恢复。

其中findSlotsToResumeLocked的逻辑这里暂且先放一放,先来看看这个包装的Emitter类对象。

Emitter是一个能够在挂起函数被取消时执行回收函数的数据发射器包装类,在挂起函数被取消时执行发射器缓存的清理工作。

Emitter内部原理.png

历史缓存

SharedFlow发射数据逻辑的分析中,会两次到调用tryEmitLocked进行尝试进行直接“发送数据”。

但这里似乎还并没有看到缓存的身影,而所谓的历史数据缓存又是什么呢?继续深入到tryEmitLocked看看。

tryEmitLocked内部实现.png

其中minCollectorIndex变量表示StateFlow数据流的所有订阅收集器,已接收到的数据在缓存数组内的最小索引,默认等于replayIndex

bufferSize表示的已缓存数量已经达或超到设置的bufferCapacity最大缓存容量时,并且订阅收集器还有值需要分发,就会执行背压策略

否则添加新元素,也即是“发送数据”,实际上添加新元素到缓存数组的逻辑在enqueueLocked函数内。

SharedFlowImpl源码enqueueLocked.png

其中的totalSize变量,表示当前缓存数组长度 + 待发送的发射器数量

private val totalSize: Int get() = bufferSize + queueSize

每次在数组添加新元素时,会检查缓存数组容量,默认会先创建容量为2的数组

如果数组容量满后以当前容量的2的倍数进行扩容

历史缓存数据则是每次调用都在这个缓存数组buffer中取出最新的replay个数元素的组成一个List

SharedFlowImpl源码replayCache(2).png

如果缓存策略为BufferOverflow.DROP_OLDEST:

tryEmitLocked中,已缓存的数量bufferSize超出允许保留的总缓存容量bufferCapacity,就会调用dropOldestLocked函数抛弃最早发送的数据缓存。

dropOldestLocked内部抛弃数据只是将数组索引的元素置空缓存数组总长度不变

收集数据

有了生产数据到缓存的功能,那么自然需要有消费数据,从缓存取出数据的地方,而在Flow中进行数据订阅,自然是从collect函数切入。

SharedFlowImpl源码collect.png

内部逻辑实现很清晰,通过分配绑定一个Slot,在轮询中尝试从缓存数组中取值并通过emit函数发送到下游。

SharedFlowcollect内是个无限循环,会一直尝试从缓存中取值,所以collect会一直处于挂起状态,直到所在协程关闭。

而这个Slot则是种收集器状态工具,与在数据流消费者进行绑定,并记录当前分发数据在缓存数组的索引。

SharedFlowSlot源码.png

SharedFlow的父类AbstractSharedFlow内,利用数组缓存了这些收集器状态记录工具,并实现了allocateSlot函数,用于分配绑定到收集器。

重新绑定收集器时,会将Solt内记录的下游重置为最新值索引的前replay个元素索引,用于确保下游会先由历史数据开始接收数据

分发数据

继续回到在轮询分发数据的逻辑中,从缓存数组中取出元素的tryTakeValue方法,依旧是synchronized加锁取值,并将待发送元素在缓存数组的索引+1。

tryTakeValue内部实现.png 其中,当前订阅收集器中需要分发数据所在的缓存索引,核心逻辑都在tryPeekLocked方法内,存在4种情况:

  1. 当前分发索引在小于缓存池的索引,则正常返回最新的索引值
  2. 超出有效缓存,并且缓存池容量大于0,则表示当前订阅收集器已经分发完了所有缓存数数据
  3. 缓存池容量为0,则在第一个订阅收集器内立即分发新的索引,后续所有订阅器都会处于空闲状态
  4. 缓存池容量为0,如果没有挂起等待添加缓存的数据,自然也就处于空闲状态

tryPeekLocked内部实现.png

如果此时没有需要发送的新值,会返回-1,即处于空闲状态,进而调用挂起函数awaitValue,创建一个新的挂起函数,内部是一段加锁的线程安全逻辑。

  • 先再次检测是否存在需要发送的值,有值则直接恢复挂起函数执行
  • 还是为空闲状态,则进入挂起状态等待缓存数组中添加数据后,再恢复挂起执行,此时外部的无限循环自然也会进入挂起等待阶段。

SharedFlowImpl源码awaitValue.png

反之,则调用getPeekedValueLockedAt函数,会从缓存中取出指定索引的值,并继续在下一次循环时取出数据。

 private fun getPeekedValueLockedAt(index: Long): Any? =
         when (val item = buffer!!.getBufferAt(index)) {
             is Emitter -> item.value //等待添加到缓存的值
             else -> item
         }

但光是取出缓存数据自然还是不够的,在空闲状态时还存在挂起等待的协程体在等待恢复,所以这里还调用了updateCollectorIndexLocked函数,去遍历检索所有需要恢复的挂起函数。

updateCollectorIndexLocked函数比较长,大体上分为3个步骤:

  1. 计算收集器需要分发的最小索引

  2. 计算需要恢复的最大挂起函数的数量

  3. 获取所有需要恢复的挂起函数协程体,并添加到待恢复的协程体数组

    • 如果存在前面发射的挂起等待发送Emitter,则此时也需要添加在需要恢复的协程体数组内,并将其中的等待添加的数据添加到缓存数组内,移除对应索引的Emitter
    • 否则协程体数组内只有正在挂起等待数据的收集器协程体

updateCollectorIndexLocked内部实现.png

小结

SharedFlow的出现,意味着一对多数据流传递成为可能,而且还能享受到Flow操作符带来的便利。

不过SharedFlow内部并不是基于Channel,而是基于数组+synchronized

相比内部基于ChannelChannelFlow

  • 两者都是线程安全的,Channel使用ReentrantLock+CAS,而SharedFlow使用synchronized

  • Channel内部是无锁双向链表结构,每个节点都是CAS引用类型,SharedFlow则是数组结构,允许存在一个从数组中截取的历史缓存集合。

  • 由于SharedFlow在订阅前就允许添加数据到缓存数组,根据replay参数的设置,可能存在部分数据遗漏的问题;而ChannelFlow只在的下游订阅后才开始发送数据,默认就能接收到所有数据。

  • 两者的订阅逻辑都通过轮询进行分发数据。

    • ChannelFlow轮询取出Channel内缓存的数据逐个分发到下游,而且这个过程是在新创建的协程中执行的,允许切换CoroutineContext
    • SharedFlow轮询取出内部缓存数组的数据,没有数据则将订阅挂起,直到上游再次发送数据。
  • SharedFlow热流ChannelFlow冷流

    • SharedFlow允许多个消费者订阅同一个数据流,且订阅消费者所在协程不会自动关闭;
    • ChannelFlow只允许单个消费者订阅,数据分发完后自动关闭订阅消费,结束数据流。

虽说目前事件总线的概念由于过渡滥用令人相当厌恶,不过SharedFlow对于这类场景就和以前的RxBusEventBus那样好用,并且还是线程安全的。

严格规范限制其使用作用范围,进行读写分离,限定为特定场景的事件分发机制,比如从定位数据只取出一次数据,分发给所有订阅者的场景,还是不错的选择。

StateFlow

当需要多个消费者都只订阅到最新的一个值,并接收后续传递的所有值,同时还能不需要订阅也能直接获取最新值时,在RxJava时代我们还有BehaviorSubject的选项,亦或是后来职责更加单一的LiveData

而在有了Flow之后,官方提供了另一个更便捷的类——StateFlow,来处理这种场景。

StateFlow实际上是SharedFlow的子类,同样也拥有只读可读可写的两种类型,StateFlowMutableStateFlow

StateFlow定义.png

从接口定义上,StateFlowLiveData的定义可谓是非常相似。

相同点:

  • 都允许多个消费者
  • 都有只读与可变类型
  • 永远只保存一个状态值
  • 同样支持DataBindingStateFlow需要新版本才支持)

LiveData不同的是:

  • 强制要求初始默认值
  • 支持CAS模式赋值
  • 默认支持防抖过滤
  • value的空安全校验
  • Flow丰富的异步数据流操作
  • 默认没有Lifecycle支持,flowcollect是挂起函数,会一直等待数据流传递数据
  • 线程安全LiveDatapostValue虽然也可在异步使用,但会导致数据丢失。

LiveData除了对于Lifecycle的支持,StateFlow基本都是处于全面碾压的态势。

使用

作为SharedFlow的子类,StateFlow在使用上与其父类基本相同。

同样是利用同名工厂函数的进行创建,只是相比SharedFlowStateFlow必须设置默认初始值

MutableStateFlow工厂方法.png

而且MutableStateFlow是无法配置缓冲区的,或者说固定永远只有一个,只会缓存最新的值

同时我们需要屏蔽外部发送污染数据,只对外部提供只读属性的StateFlow,此时就需要asStateFlow

 fun test() = runBlocking{
     val stateFlow = MutableStateFlow(1)
     val readOnlyStateFlow = stateFlow.asStateFlow()
     
     //模拟外部立即订阅数据
     val job0 = launch {
         readOnlyStateFlow.collect { println("collect0 : $it") }
     }
     delay(50)
     //模拟在另一个类发送数据
     launch {
         for (i in 1..3){
             println("wait emit $i")
             stateFlow.emit(i)
             delay(50)
         }
     }
     //模拟启动页面,在新页面订阅
     delay(200)
     val job1 = launch {
         readOnlyStateFlow.collect{ println("collect1 : $it") }
     }
     val job2 = launch {
         readOnlyStateFlow.collect{ println("collect2 : $it") }
     }
     println("get value : ${readOnlyStateFlow.value}")
     
     delay(200)
     job0.cancel()
     job1.cancel()
     job2.cancel()
 }
 ​
 collect0 : 1
 wait emit 1
 wait emit 2
 collect0 : 2
 wait emit 3
 collect0 : 3
 get value : 3
 collect1 : 3
 collect2 : 3
 ​

可以看到,在没有发送数据时订阅会先接收默认值

而新发送的数据后,由于第一个值与原有值相同,直接被过滤掉了。

后续新添加的订阅者能够接收到的就只有最新的值

StateFlow订阅者所在的协程,最好使用独立协程collect会一直挂起,协程内的后续操作不会执行

冷流转换热流

StateFlow同样也有由Flow冷流转化为热流的操作符函数stateIn

stateIn源码.png

shareIn函数的区别只是必须设置默认值stateIn转化的共享数据流只缓存一个最新值。

StateFlow原理分析

StateFlow内部并没有SharedFlow的缓存数组,只是用atomic引用类型的状态值,永远只保留一个最新的值。

StateFlowImpl源码构造函数.png

由于只保存了一个值,可以通过对value对象进行取值与赋值操作。

发送数据

所有发送数据操作tryEmitemit都是调用setValue操作,并最终调用updateState函数进行CAS状态赋值。

StateFlowImpl源码updateState.png

函数看着比较长,其内部很巧妙的根据一个sequence更新序号与synchronized加锁,配合无限循环,只允许在更新序号为偶数才正常进行更新流程,并最终更新序号为奇数。

如果本身更新序号就为奇数,则表示已经执行过更新流程,直接跳过后续流程。

收集器状态

StateFlowImpl的父类同样也是AbstractSharedFlow,不同于SharedFlow,这里的消费者状态工具是AbstractSharedFlowSlot的另一个实现——StateFlowSlot

StateFlowSlot源码.png

这个Slot内部同样是atomicCAS引用类型,其允许有四种状态

  • null - 表示已经空闲释放,可以分配给消费者收集器
  • NONE - 表示已经分配给消费者接收器
  • PENDING - 表示上游已更新新值,待发送给收集器
  • CancellableContinuationImpl - 表示收集器已挂起在等待上游数据

StateFlow更新状态值的流程中,会遍历所有已分配的Solt,调用makePending尝试将所有已分配的Solt状态CAS更新为PENDING使消费端准备好接收数据

StateFlowSlot源码makePending(new).png

收集数据

Flow数据流消费端收集数据,自然还是使用collect

StateFlowImpl源码collect.png

在消费者订阅函数,如SharedFlow相同,会一直循环等待上游数据

仅在刚订阅时,StateFlow立即发送最新数据。随后每次发送数据前都会进行重复过滤,并进行空安全检查

随后也是同样的调用awaitPending函数,创建新协程并进入挂起状态,直到上游重新传递数据将该协程恢复。

StateFlowSlot源码awaitPending.png

关联生命周期

早在RxJava时代,如果直接在视图中调用subscribe订阅数据流,若是视图生命周期处于后台状态时,接收数据更新就可能会出现不符合预期的情况。

所以在LiveData出现后,在视图层的数据订阅都逐渐移交给LiveData了,而RxJava逐渐退居到数据层进行数据逻辑处理。

现在的Flow在视图内调用collect订阅数据流自然也会存在与RxJava相同的问题。

因此我们需要让Flow在视图生命周期处于后台时,不对数据流进行订阅处理、不传递数据到消费端。

LifecycleCoroutineScope

在早期,Lifecycle库提供了拓展属性coroutineScope

作为视图专用的LifecycleCoroutineScope协程作用域,会在视图销毁时取消协程作用域,其中拥有多个launchWith系列函数。

 public val Lifecycle.coroutineScope: LifecycleCoroutineScope
     
 public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope {
     ...
     public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {
         lifecycle.whenCreated(block)
     }
     
     public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {
         lifecycle.whenStarted(block)
     }
     
     public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
         lifecycle.whenResumed(block)
     }
     ...
 }

比如launchWhenStarted函数,会将Flow数据流消费端所在的协程,函数执行限定在小于Lifecycle.State.STARTED状态下。(图片来自网络)

Lifecycle-State.png

也即是消费者端所在协程内的collect函数内部逻辑,只会在视图处于onStart生命周期后才恢复执行,而处于后台的生命周期会被暂时挂起

但对于Flow数据流来说,即便消费端的处理逻辑挂起了,生产端数据源依然还在执行,尤其是某些数据源一直运行的场景可能造成不必要的资源损耗,官方目前也并不推荐使用这些函数

Android Studio 2021.1.1 Patch 1中,如果在编译器自带的DataBinding版本内中使用StateFlow,自动生成的Binding文件内,依然还是会使用launchWhenCreated函数进行订阅收集数据流变化。

 //ViewDataBindingKtx.kt
 internal class StateFlowListener(
          binder: ViewDataBinding?,
          localFieldId: Int,
          referenceQueue: ReferenceQueue<ViewDataBinding>
  ) : ObservableReference<Flow<Any?>> {
  ...
  override fun addListener(target: Flow<Any?>?) {
      val owner = _lifecycleOwnerRef?.get() ?: return
      if (target != null) {
          startCollection(owner, target)
      }
  }
  ...
  private fun startCollection(owner: LifecycleOwner, flow: Flow<Any?>) {
      observerJob?.cancel()
      //在onCreate -> onDestroy之间执行该协程代码块
      observerJob = owner.lifecycleScope.launchWhenCreated {
          flow.collect {
              //更新databinding数据,执行requestBinding
              listener.binder?.handleFieldChange(listener.mLocalFieldId, listener.target, 0)
          }
      }
  }
 }
 ​

repeatOnLifecycle

随着Lifecycle-runtime-ktx库更新至2.4.0版本,Lifecycle提供了一个新的拓展函数repeatOnLifecycle

repeatOnLifecycle源码解析.png

看着代码实现很多,内部逻辑其实很简单,切换CoroutineContext到UI主线程,在进入允许的生命周期状态时,启动协程,订阅数据流。在超出设定的生命周期状态后,关闭协程,取消订阅

与其他方式的比较:(图片来自官方)

repeatOnLifecycle原理.png

对于共享数据流的StateFlow来说,每次订阅都只会获取最新的值,这也更接近LiveData的使用逻辑,也即所谓的粘性数据

repeatOnLifecycle函数出现后,官方也开始计划删除launchWith系列函数。

除此之外,Lifecycle库还提供了一个Flow的中间操作符flowWithLifecycle,利用callbackFlow来内部调用repeatOnLifecycle

callbackFlow内部实际上是基于Channel实现的,对于上游数据流具有缓冲区的作用

flowWithLifecycle源码解析.png

  • 对于单个Flow数据流的生命周期控制,flowWithLifecycle操作符可以很好解决样板代码。
  • 如果需要同时控制多个Flow数据流的生命周期,还是推荐使用repeatOnLifecycle避免重复创建Channel
  • 冷流Flow不推荐直接使用flowWithLifecycle,避免多次创建新的数据源。

于是Flow数据流就可以在ActivityFragment中很方便的绑定视图生命周期

 override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      //模拟从viewModel开放出来的状态更新
      viewModel.readOnlyStateFlow
             //在onStart开启订阅上游数据流,onPause取消订阅
             .flowWithLifecycle(lifecycle,Lifecycle.State.STARTED) 
             .onEach { //do something }
             .launchIn(lifecycleScope) //运行在主线程的协程作用域,在视图销毁时自动取消作用域
 }

总结

至此,作为Kotlin Flow的最后一部分拼图——共享数据流也就此集齐了。

  • SharedFlow作为允许保留历史缓存并且只能收到新数据的存在,对于一对多事件分发的场景是个很好的选择。
  • StateFlow则是与原本LiveData的定位重合,永远只持有最新数据,更适用于处理状态更新

配合repeatOnLifecycle限制视图生命周期订阅,StateFlow可以完全替代LiveData,更新视图的状态显示,同时支持粘性数据

而原本需要封装LiveData才能处理的不需要粘性单次执行事件,只需要将replay设置为0,SharedFlow就能很好的承担这个职责。

当然,如果不需要Flow数据流操作与线程安全的需求,像LiveData这样职责单一的类,承担视图状态更新也还是不错的选择,简单也意味着不容易出错,便于维护。

毕竟StateFlow到底还是要依靠Kotlin协程来实现,LiveData利用observer能直接订阅状态还是比较方便的。

总的来说,随着Kotlin Flow的出现,从数据源的逻辑处理到视图层的状态与事件订阅,都有了很好的新选择。

也唯有熟悉与理解其背后运作机制,才能更好在合适的场景中灵活运用。

参考资料

StateFlow 和 SharedFlow

使用更为安全的方式收集 Android UI 数据流

不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比

从 LiveData 迁移到 Kotlin 数据流

Flow 操作符 shareIn 和 stateIn 使用须知