Flow学习

LiveData的不足

LiveData结构简单,但是不够强大,它有以下不足

  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦
  2. LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘
  3. LiveData 只能在主线程更新数据: 只能在主线程 setValue,即使 postValue 内部也是切换到主线程执行;
  4. LiveData 数据重放问题: 注册新的订阅者,会重新收到 LiveData 存储的数据,这在有些情况下不符合预期(可以使用自定义的 LiveData 子类 SingleLiveData 或 UnPeekLiveData 解决,此处不展开);
  5. LiveData 不防抖: 重复 setValue 相同的值,订阅者会收到多次 onChanged() 回调(可以使用 distinctUntilChanged() 解决,此处不展开);
  6. LiveData 不支持背压: 在数据生产速度 > 数据消费速度时,LiveData 无法正常处理。比如在子线程大量 postValue 数据但主线程消费跟不上时,中间就会有一部分数据被忽略。
  7. Flow可以跟LiveData结合使用,LiveData的生命周期在OnDestory前不会自动取消订阅,在一些UI相关场景没有问题,可以使用LiveData。Flow则更加强大,可以在onPause等时机中止上游流,节省资源

Flow,LiveData说白了都是观察者模式,在ViewModel中生产数据,在View中观察数据,Flow生产数据时不用切换线程,LiveData生产数据时会切换到主线程,这就是区别。

Flow 支持数据重放配置: Flow 的子类 SharedFlow 支持配置重放
replay:能够自定义对新订阅者重放数据的配置

好文章

Kotlin协程之Flow使用 - 掘金

官方推荐取代LiveData,有必要吗

有小伙伴说看不懂 LiveData、Flow、Channel,跟我走

Kotlin Flow响应式编程,基础知识入门-郭霖

Flow

用法:使用flow {…} 函数创建了一个冷数据流Flow,通过emit来发射数据,然后通过collect函数来收集这些数据

1
2
3
4
5
6
7
8
9
10
fun test() {
lifecycleScope.launch {
flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}.collect { value -> Log.d(TAG, "value :${value}") }
}
}

线程切换

  • flowOn只影响前面没有自己上下文的操作符。已经有上下文的操作符不受后面flowOn影响。
  • 不管 flowOn 如何切换线程, collect 始终是运行在调用它的协程调度器上。
  • flowOn 下一行开始生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun test() {
lifecycleScope.launch {
flow {
for (i in 1..3) {
Log.d(TAG, "flow :${ currentCoroutineContext()}")
delay(100) // io
emit(i)
}
}.flowOn(Dispatchers.IO)
.map {
Log.d(TAG, "map :${ currentCoroutineContext()}") //default
it
}.flowOn(Dispatchers.Default)
.collect { value ->
Log.d(TAG, "collect:${ currentCoroutineContext()} value :${value}") //主线程
}
}
}

常用操作符

过度操作符:又或者叫做流程操作符,用来区分流程执行到某一个阶段。比如:onStart/onEach/onCompletion。过渡操作符应用于上游流,并返回下游流。这些操作符也是冷操作符,就像流一样。这类操作符本身不是挂起函数。它运行的速度很快,返回新的转换流的定义。
异常操作符:用来捕获处理流的异常。比如:catch,onErrorCollect(已废弃,建议用catch)。
转换操作符:主要做一些数据转换操作。比如:transform/map/filter/flatMapConcat等
限制操作符:流触及相应限制的时候会将它的执行取消。比如:drop/take等
末端操作符:是在流上用于启动流收集挂起函数。==collect 是最基础的末端操作符==,但是还有另外一些更方便使用的末端操作符。例如:toList、toSet、first、single、reduce、fold等等

1
2
3
4
5
6
7
8
9
10
11
fun test() {
val flow1 = (1..3).asFlow()
val flow2 = flowOf("one", "two", "three")
lifecycleScope.launch {
flow2.zip(flow1) { value1, value2 ->
"$value1 :$value2"
}.collect { value ->
Log.d(TAG, "collect :${value}")
}
}
}

D/carman: collect :1 :one
D/carman: collect :2 :two
D/carman: collect :3 :three

直接使用Flow的局限性

虽然Flow可以将任意的对象转换成流的形式进行收集后计算结果。但是如果我们是直接使用Flow,它一次流的收集是我们已知需要计算的值,而且它每次收集完以后就会立即销毁。我们也不能在后续的使用中,发射新的值到该流中进行计算。

为什么要使用 StateFlow 和 ShareFlow?

[[冷流热流]]

我们希望状态的变动都能通知到会有所动作的观察者。
StateFlow和ShareFlow也是Flow API的一部分,它们允许数据流以最优方式,发出状态更新并向多个使用方发出值。
与使用flow构建器构建的冷数据流不同,StateFlow是热数据流。
StateFlow是继承自SharedFlow,我们可以把StateFlow看作为SharedFlow一个更佳具体实现。

操作符

buffer

有个需求:上游发射数据速度高于下游,如何提升发射效率?

使用buffer操作符解决背压问题

buffer原理简单来说:

构造了新的协程执行flow闭包,上游数据会发送到Channel 缓冲区里,发送完成继续发送下一条
collect操作符监听缓冲区是否有数据,若有则收集成功
原理是基于ChannelFlow

上游生产速度很快,下游消费速度慢,我们只关心最新数据,旧的数据没价值可以丢掉。 使用conflate操作符处理

这一次,让Kotlin Flow 操作符真正好用起来 - 掘金
image.png|1100


Flow学习
http://peiniwan.github.io/2024/04/b3f96b27a7fc.html
作者
六月的雨
发布于
2024年4月6日
许可协议