玩转 Flow 操作符(二):时间控制、聚合与组合

时间相关: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

TimeoutCancellationExceptionCancellationException 的子类,如果不进行捕获会导致取消当前协程。

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

有时候,我们并不关心数据流的过程,只关心它的最终结果。这时,就可以用到 reducefold

它们都是终端操作符 ,会启动 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:发射每一步的累积结果

runningFoldrunningReducefoldreduce 的中间操作符版本。

它们会返回一个新的 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:并发合并数据流内容

mergeflattenMerge 用于并发 收集所有流,其中 merge 用于处理 Iterable<Flow<T>>,也就是一个或多个 Flow

合并后的 Flowcollect 时,会同时启动并收集所有被合并的 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

flatMapMergeflatMapConcatmap + 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

flatMapLatestflatMapMerge 都是并发处理,但区别在于:当上游发射了一个新的内部 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
相关推荐
Jerry4 小时前
Compose 基础知识章节合集
android
Jerry4 小时前
Compose 布局、主题设置和动画测试
android
Zender Han5 小时前
Flutter 状态管理详解:深入理解与使用 Bloc
android·flutter·ios
程序员江同学5 小时前
Kotlin 技术月报 | 2025 年 10 月
android·kotlin
RickyWasYoung6 小时前
【matlab】字符串数组 转 double
android·java·javascript
bluetata7 小时前
Rokid AR眼镜开发入门:构建智能演讲提词器Android应用
android·人工智能·云计算·ar·ai编程
马 孔 多 在下雨8 小时前
手机App上的轮播图是如何实现的—探究安卓轮播图
android·智能手机
00后程序员张8 小时前
iOS 26 开发者工具推荐,构建高效调试与性能优化工作流
android·ios·性能优化·小程序·uni-app·iphone·webview
小范馆10 小时前
通过 useEventBus 和 useEventCallBack 实现与原生 Android、鸿蒙、iOS 的事件交互
android·ios·harmonyos