时间相关:timeout, sample 与 debounce
timeout:为数据流设置超时
timeout 操作符会为 Flow 设置一个超时。
-
从
collect开始计时,如果在规定时间内没有数据被发出,它会抛出一个TimeoutCancellationException。 -
当一条数据被发出后,它会重新开始计时。
它的用途在于监控一个持续的数据流或"心跳",比如监控设备的在线状态。
kotlin
/**
* 心跳数据包
*/
data class Packet(val timestamp: Long, val data: String)
private fun getHeartbeatStreamForDevice(deviceName: String): Flow<Packet> = flow {
while (true) {
if (System.currentTimeMillis() % 100 > 80) { // 模拟设备掉线
delay(Long.MAX_VALUE)
}
// 来自设备的心跳数据包
emit(Packet(System.currentTimeMillis(), "$deviceName: I'm live"))
delay(5000)
}
}
fun main(): Unit = runBlocking {
// 设备的数据流
val deviceHeartbeatFlow: Flow<Packet> = getHeartbeatStreamForDevice("device-123")
try {
deviceHeartbeatFlow
.timeout(10.seconds) // 设置10秒超时
.collect { packet ->
// 只要收到数据包,就更新UI并重置计时器
println("Device 123: Online, message is ${packet.data}")
}
} catch (e: TimeoutCancellationException) {
// 10秒内没有新数据包,判定设备已离线
println("Device 123: Offline - Connection Lost")
}
}
运行结果可能为:
less
Device 123: Online, message is device-123: I'm live
Device 123: Online, message is device-123: I'm live
Device 123: Online, message is device-123: I'm live
Device 123: Offline - Connection Lost
TimeoutCancellationException 是 CancellationException 的子类,如果不进行捕获会导致取消当前协程。
sample:按固定周期采样
我们设置一个时间窗口,从 collect 开始,sample(采样)会每隔一段时间掐表一次,只发射这个时间窗口内接收到的最新数据,其余数据全部丢弃。
如果在这个窗口内,没有收到任何数据,就不会发射。
kotlin
fun main(): Unit = runBlocking {
flow {
emit(1)
delay(300)
emit(2)
delay(300)
emit(3)
delay(500)
emit(4)
emit(5)
delay(1500)
emit(6) // 在掐表前,Flow 结束了,所以这条数据会被丢弃
}.sample(1.seconds).collect {
println(it)
}
}
运行结果:
less
3
5
它比较适合用于定时刷新的场景,比如股价显示应用,1 秒内数据可能会改变 100 次,如果只是想每秒更新一次界面,就可以用它。
debounce:事件防抖
debounce(防抖)操作符的效果是:
-
每收到一条数据时,先不发送,而是压住并启动一个计时器。
-
如果在计时器时间内,又有新数据到来,它会丢弃之前压住的旧数据,并重置计时器。
-
当计时器时间到了之后(期间没有新数据到来),它才会将最后被压住的数据发送出去。
kotlin
fun main(): Unit = runBlocking {
flow {
emit(1)
delay(300)
emit(2)
delay(300)
emit(3)
delay(500)
emit(4)
emit(5)
delay(1500)
emit(6)
}.debounce(1.seconds).collect {
println(it)
}
}
运行结果:
less
5
6
它的典型场景就是搜索提示,只有在用户停止输入的一段时间后,才会拿着关键词去请求搜索建议。
注意:
debounce不适合做按钮点击防抖,因为debounce在抖动停止后 才会响应,用户会感觉到卡顿或是延迟。按钮防抖用的是throttleFirst,它会在第一次点击时立即响应,然后在之后的一段时间内,忽略后续的点击。
kotlin
// throttleFirst 的实现
fun <T> Flow<T>.throttleFirst(timeWindow: Duration): Flow<T> = flow {
// 记录上一次发射的时间
var lastEmitTime = 0L
collect {
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmitTime > timeWindow.inWholeMilliseconds) {
emit(it)
lastEmitTime = currentTime
}
// 如果时间间隔小于 timeWindow,则忽略该事件
}
}
聚合:reduce 与 fold
有时候,我们并不关心数据流的过程,只关心它的最终结果。这时,就可以用到 reduce 和 fold。
它们都是终端操作符 ,会启动 Flow 的收集(collect)并返回一个单一 结果。以 fold 的内部实现为例:
kotlin
// 注意是挂起函数
public suspend inline fun <T, R> Flow<T>.fold(
initial: R,
crossinline operation: suspend (acc: R, value: T) -> R
): R {
var accumulator = initial
collect { value ->
accumulator = operation(accumulator, value)
}
return accumulator
}
reduce:将数据流归约为单一结果
reduce 会使用第一个元素作为初始的累加值,从第二个元素开始,使用我们提供的算法,将当前累加值和当前元素计算出新的累加值。
kotlin
fun main(): Unit = runBlocking {
flowOf(1, 2, 3).reduce { accumulator /*累加器:当前累加的结果*/, value/*当前值*/ ->
println("accumulator: $accumulator, value: $value")
// 当前算法
accumulator + value
}.let {
println("The sum of flow elements is $it")
}
}
运行结果:
less
accumulator: 1, value: 2
accumulator: 3, value: 3
The sum of flow elements is 6
reduce 得到的结果类型并不会改变,还是 Flow 的元素类型。
fold:提供初始值的折叠
fold(折叠)与 reduce 基本没差,不过它允许我们提供一个初始值。
这样,计算可以从第一个元素开始。另外,这也导致了返回值的类型会是初始值的类型(最终返回结果就是累加器 accumulator),可能会与 Flow 元素类型不同。
kotlin
fun main(): Unit = runBlocking {
flowOf(1, 2, 3).fold("The sum of flow elements is: ") { acc, value ->
println("acc: $acc, value: $value")
acc + value
}.let { result: String ->
println(result)
}
}
运行结果:
less
acc: The sum of flow elements is: , value: 1
acc: The sum of flow elements is: 1, value: 2
acc: The sum of flow elements is: 12, value: 3
The sum of flow elements is: 123
流转换:running...
runningFold / runningReduce:发射每一步的累积结果
runningFold 和 runningReduce 是 fold 和 reduce 的中间操作符版本。
它们会返回一个新的 Flow,每一步计算的结果都会被发送到这个 Flow 中。
kotlin
fun main(): Unit = runBlocking {
println("...Running reduce...")
flowOf(1, 2, 3).runningReduce { acc, value ->
acc + value
}.collect {
println(it)
}
println("...Running fold...")
flowOf(1, 2, 3).runningFold(60) { acc, value ->
acc + value
}.collect {
println(it)
}
}
运行结果:
less
...Running reduce...
1
3
6
...Running fold...
60
61
63
66
合并多个 Flow:merge、zip 与 combine
合并
这类操作符用于将多个流的数据项铺平到一个流中。
merge / flattenMerge:并发合并数据流内容
merge、flattenMerge 用于并发 收集所有流,其中 merge 用于处理 Iterable<Flow<T>>,也就是一个或多个 Flow。
合并后的 Flow 在 collect 时,会同时启动并收集所有被合并的 Flow。
kotlin
private suspend fun delayRandomly(longRange: LongRange) {
delay(longRange.random())
}
private val numbers = 300L..1000L
fun main(): Unit = runBlocking {
val flow1 = flow {
listOf(1, 2, 3).forEach {
delayRandomly(numbers)
emit(it)
}
}.map {
"from flow1: $it"
}
val flow2 = flow {
listOf("a", "b", "c").forEach {
delayRandomly(numbers)
emit(it)
}
}.map {
"from flow2: $it"
}
val mergedFlow = merge(flow1, flow2)
mergedFlow.collect {
println(it)
}
}
运行结果可能为:
less
from flow2: a
from flow1: 1
from flow1: 2
from flow2: b
from flow2: c
from flow1: 3
也可以调用 Iterable<Flow<T>>.merge() 扩展函数来合并数据流,例如:
kotlin
val merge = listOf<Flow<*>>(flow1, flow2).merge()
flattenMerge 用于处理 Flow<Flow<T>>,其中 Flow 的元素类型是 Flow<T>。
kotlin
private val numbers = 300L..1000L
private val numbers2 = 100..300L
@OptIn(ExperimentalCoroutinesApi::class)
fun main(): Unit = runBlocking {
val flattenMergedFlow = flow {
for (i in 1..3) {
delayRandomly(numbers2)
emit(i)
}
}.map { element ->
flow {
for (i in 0..element) {
delayRandomly(numbers)
emit(i)
}
}.map {
"from flow$element, value is $it"
}
}.flattenMerge()
flattenMergedFlow.collect {
println(it)
}
}
运行结果可能为:
kotlin
from flow2, value is 0
from flow3, value is 0
from flow1, value is 0
from flow3, value is 1
from flow2, value is 1
from flow3, value is 2
from flow2, value is 2
from flow1, value is 1
from flow3, value is 3
flattenConcat:顺序合并数据流内容
flattenConcat 用于顺序 收集数据流,它会等待前一个内部 Flow 完全结束后,再开始收集并转发第二个后一个内部 Flow。
kotlin
@OptIn(ExperimentalCoroutinesApi::class)
fun main(): Unit = runBlocking {
val flattenConcatedFlow = flow {
for (i in 1..3) {
delayRandomly(numbers2)
emit(i)
}
}.map { element ->
flow {
for (i in 0..element) {
delayRandomly(numbers)
emit(i)
}
}.map {
"from flow$element, value is $it"
}
}.flattenConcat()
flattenConcatedFlow.collect {
println(it)
}
}
运行结果:
less
from flow1, value is 0
from flow1, value is 1
from flow2, value is 0
from flow2, value is 1
from flow2, value is 2
from flow3, value is 0
from flow3, value is 1
from flow3, value is 2
from flow3, value is 3
flatMapMerge 与 flatMapConcat
flatMapMerge 和 flatMapConcat 是 map + flatten... 的快捷组合,以 flatMapConcat 为例:
kotlin
val flattenConcatedFlow = flow {
for (i in 1..3) {
delayRandomly(numbers2)
emit(i)
}
}.flatMapConcat { element ->
flow {
for (i in 0..element) {
delayRandomly(numbers)
emit(i)
}
}.map {
"from flow$element, value is $it"
}
}
flatMapLatest
flatMapLatest 和 flatMapMerge 都是并发处理,但区别在于:当上游发射了一个新的内部 Flow 时,它会立即取消当前正在收集的旧 Flow,转而只收集这个最新的 Flow。
将之前的代码中的 flattenConcat 换成 flatMapLatest,运行结果可能为:
less
from flow3, value is 0
from flow3, value is 1
from flow3, value is 2
from flow3, value is 3
结合
这类操作符用于将两个或多个不同的流,根据某种规则进行配对,生产新的数据。
combine:使用最新值结合
combine 会等所有流都至少发射了一个值后开始结合。任何一个流发射了新数据,它都会与其他流的最新值进行结合,并将结果发射出去。
kotlin
fun main(): Unit = runBlocking {
val flow1 = flow {
delay(300)
emit("a")
delay(500)
emit("b")
delay(200)
emit("c")
}
val flow2 = flow {
delay(400)
emit(1)
delay(500)
emit(2)
delay(300)
emit(3)
}
val combinedFlow = combine(flow1, flow2) { a, b ->
"$a - $b"
}
combinedFlow.collect {
println(it)
}
}
运行结果:
less
a - 1
b - 1
b - 2
c - 2
c - 3
它特别适合 UI 状态的聚合。就比如一个登录表单中,登录按钮是否可用,取决于用户用户名是否合法和密码是否合法。
zip:严格的一对一配对
zip 像拉链一样,会进行严格的一对一配对,也就是严格让两个流的第 n 个元素进行配对。
如果其中一个流先结束了,zip 会立即结束。
kotlin
fun main(): Unit = runBlocking {
val flowA = flowOf("A", "B", "C")
val flowB = flowOf(1, 2, 3, 4) // 4 将被丢弃
flowA.zip(flowB) { a, b -> "$a-$b" }
.collect {
println(it)
}
}
运行结果:
less
A-1
B-2
C-3