前言:
前两篇文章我们介绍了Flow的基础知识以及Flow的基础操作符。什么是冷流?什么是热流?onEach、filter、map、debouce、catch等基础操作符,感兴趣的同学可以去阅读下这两篇文章:
第一节:Flow的基础知识
第二节:Flow的基础操作符
这篇文章我们来介绍下Flow中一些比较高级的操作符,熟练的掌握这些操作符,可以让我们在实际开发中,像使用网络请求或者异步数据处理带来很大的方便。
1.操作符flowOn
flowOn操作符用于更改流发射的上下文(即改变流执行的协程调度器)。它会改变流上游的协程调度器,但不会影响下游。如下代码示例,我们使用flow操作符,让流的数据发送在子线程中执行,流的收集在主线程中执行(main函数在主线程中运行):
fun main() = runBlocking {
flow {
// 网络请求或其它耗时操作
emit("flowOn")
println("emit threadName = ${Thread.currentThread().name}")
}
.flowOn(Dispatchers.IO)
.collect {
println(println("collect threadName = ${Thread.currentThread().name}"))
}
}
// 输出
emit threadName = DefaultDispatcher-worker-1
collect threadName = main
kotlin.Unit
2.操作符lauchIn
launchIn 是 Kotlin Flow 的终端操作符 ,用于在指定协程作用域中异步启动流收集 。它与 collect 功能相同,但通过结构化并发提供更简洁的生命周期管理。
// 传统方式(可能阻塞)
scope.launch {
flow.collect { ... }
}
// 使用 launchIn(立即返回,不阻塞)
flow.onEach { ... }.launchIn(scope)
流收集与作用域绑定,自动取消:
val scope = CoroutineScope(Dispatchers.Main)
flow
.onEach { updateUI(it) }
.launchIn(scope) // 当 scope 取消时,流自动取消
// 取消整个作用域(自动终止流收集)
scope.cancel()
launchIn(lifecycleScope) // 绑定 Activity/Fragment 生命周期
launchIn(viewModelScope) // 绑定 ViewModel 生命周期
常用的调用示例:
dataFlow
.onStart { showLoading() }
.onEach { updateData(it) }
.catch { handleError(it) }
.onCompletion { hideLoading() }
.launchIn(viewModelScope)
3.操作符flatMapConcat
前面我们所介绍的内容都是在一个flow上进行操作的,从这里我们将开始介绍对多个流组合使用的操作符。flatMapConcat操作符用于将前一个flow与后一个flow映射,拼接成一个flow,且保证两个flow中的数据执行始终是并行的。
fun main() = runBlocking {
flowOf(200L, 100L)
.flatMapConcat { timeMills ->
flow {
delay(timeMills)
emit("first: $timeMills ms")
emit("second: $timeMills ms")
}
}
.collect { println(it) }
}
// 输出
first: 200 ms
second: 200 ms
first: 100 ms
second: 100 ms
flatMapConcat操作符,接收其上游的数据,并返回一个新的flow:
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
map(transform).flattenConcat()
4.操作符flatMapMerge
flatMapMerge和flatMapConcat在用法上几乎是一致的,从命名上我们可以看出它们的区别在于merge是合并的意思,而concat是连接的意思。合并是不保证执行顺序的,而连接是可以保证前后数据的顺序执行。同样的我们使用上面flatMapConcat的示例将其操作符改为flatMapMerge,输出结果就会完全不同:
fun main() = runBlocking {
flowOf(200L, 100L)
.flatMapMerge { timeMills ->
flow {
delay(timeMills)
emit("first: $timeMills ms")
emit("second: $timeMills ms")
}
}
.collect { println(it) }
}
// 输出
first: 100 ms
second: 100 ms
first: 200 ms
second: 200 ms
从这里我们可以看出flatMapMerge不会在保证两个flow前后数据发送的顺序,而是优先执行当前需要处理的任务。
5.操作符flatMapLatest
flatMapLatest也是一个组合流,用法上和前两个flatMap操作符都是一致的。当前一个flow中的数据传递到后一个flow中会立刻进行处理,但如果前一个flow中的下一个数据要发送了,而后一个flow中上一个数据还没处理完,则会直接将剩余逻辑取消掉,开始处理最新的数据。
fun main() = runBlocking {
flow {
emit("first")
delay(100)
emit("second")
delay(50)
emit("third")
}
.flatMapLatest { value ->
flow {
delay(150)
emit("value: $value")
}
}
.collect {
println("collect start.")
println(it)
println("collect finish.")
}
}
// 输出
collect start.
value: third
collect finish.
从示例代码我们可以看到,flow1发送了3条数据,但前两条数据发送结束后flow2中的数据还没有处理完成,前两条数据都被丢弃了,只有第三条数据被接收处理了。
6.操作符zip
zip操作符同样可以将两个flow拼接成一个flow,它也是并发执行两个任务,最终的执行时长以耗时较长的任务为准。如下代码示例:
fun main(): kotlin.Unit = runBlocking {
measureTime {
flow {
delay(1000)
emit("first")
}.zip(flow {
delay(3000)
emit("second")
}) { first, second ->
println("$first $second")
}.collect()
}.apply { println("spend $this ms") }
}
// 输出
first second
spend 3.084227200s ms
zip操作符还有个特点就是,当两个flow中的发射的数据量不一致时,如果其中一个flow中的数据已完全处理完成,就会立即终止运行。如下代码示例:
fun main() = runBlocking {
flowOf(1, 2, 3)
.zip(flowOf("a", "b")) { pre, next ->
println("pre = $pre, next = $next")
}.collect()
}
// 输出
pre = 1, next = a
pre = 2, next = b
flow1中有3条需要处理的数据,flow2中只有两条需要处理的数据,当flow2中数据处理完成后,当前数据流也已经终止了。
7.操作符combine
介绍完zip操作符,再来介绍combine操作符就比较简单了。首先它和zip操作符一样都是并行执行两个flow,这里就不重复上述的示例代码了,感兴趣的同学可以将上述zip操作符中的第一个代码片段改为combine自行尝试下。这里我们主要来讲述将上述第二个代码片段中的zip操作符替换为combine操作符,如下示例代码:
fun main() = runBlocking {
flowOf(1, 2, 3)
.combine(flowOf("a", "b")) { pre, next ->
println("pre = $pre, next = $next")
}.collect()
}
// 输出
pre = 1, next = a
pre = 2, next = b
pre = 3, next = b
从输出结果我们很容易看出zip操作符和combine操作符的区别。即当两个flow中执行的数据任务量不同时,当其中一个flow中的数据处理完成后,当前数据流并不会立即停止,而是将执行完成数据flow中的最后一个数据,同当前还在处理数据的flow数据一同下发。到这里关于组合流的基本使用我们就介绍完,最后我们再介绍另外两个比较重要的操作符buffer及conflate。
8.操作符buffer
默认情况下,flow数据流的发送和接收在同一个协程作用域中,我们可以理解为:数据的发送和接收是串行的,即当一条数据发送和接收处理完成后,我们才会接着处理下一条数据,如下代码示例:
fun main() = runBlocking {
flow {
for (i in 0 until 3) {
emit(i)
delay(500)
}
}.onEach { println("value:$it is ready.") }
.collect {
delay(500)
println("collect $it")
}
}
// 输出
value:0 is ready.
collect 0
value:1 is ready.
collect 1
value:2 is ready.
collect 2
这种串行的效果,有时候可能并不能够满足我们实际的开发需求。而buffer的存在为我们很好的解决了这个问题。buffer操作符可以让我们的数据发送和接收互不影响,它为数据提供了一个缓存区,数据的发送和收集运行在不同的协程作用域中。这样flow函数只需要关注数据的发送,并不需要关注这条数据是否处理完成,collect函数只管负责从buffer中收集数据就可以了。接着上面的示例,我们使用buffer操作符再看下效果:
fun main() = runBlocking {
flow {
for (i in 0 until 3) {
emit(i)
delay(500)
}
}.onEach { println("value:$it is ready.") }
.buffer()
.collect {
delay(500)
println("collect $it")
}
}
// 输出
value:0 is ready.
value:1 is ready.
collect 0
value:2 is ready.
collect 1
collect 2
由输出结果我们可以看到,数据的发送和接收不再是串行执行。
9.操作符conflate
conflate操作符是基于buffer操作符实现的,单单从源码上看,仅仅是将buffer函数中的capacity参数的值固定为CONFLATED。
public fun <T> Flow<T>.conflate(): Flow<T> = buffer(CONFLATED)
在上面我们对比了zip和combine操作符的区别,这里为了说清楚conflate操作符。我们使用collectLatest操作符做对比,如下代码示例:
fun main() = runBlocking {
flow {
var count = 0
while (true) {
emit(count)
delay(1000)
count++
}
}.collectLatest {
println("start $it collect.")
delay(2000)
println("collect $it finish.")
}
}
// 输出
start 0 collect.
start 1 collect.
start 2 collect.
从输出结果我们可以看到,当上游有新的数据发送,而下游的任务还没有处理完成,下游的任务就被取消了,collect finish不会被执行到。而conflat操作符就是用来解决这个问题的。conflat操作符会保证我们总会将流的发送和收集执行完毕,才会执行下一条任务,如果有新的任务来了,缓存中任有未处理的数据,那么就将旧的正在处理的任务丢弃转而执行新的任务。
fun main() = runBlocking {
flow {
var count = 0
while (true) {
emit(count)
delay(1000)
count++
}
}.conflate()
.collect {
println("start $it collect.")
delay(2000)
println("collect $it finish.")
}
}
// 输出
start 0 collect.
collect 0 finish.
start 1 collect.
collect 1 finish.
start 3 collect.
collect 3 finish.
start 5 collect.
collect 5 finish.
由这里的输出结果我们可以看到2,4被丢弃了,但下游的任务是在完全处理完成以后再去处理其它的任务。
总结:
到这里flow中的一些比较常用的高级操作符就介绍完了。对于
- flatMapConcat : flatMapMerge
- zip : combine
- buffer : conflat : collectLatest
我们能够清楚的掌握它们之间的区别。这对我们在实际开发中使用flow会很有帮助。
文章的质量可能并不高,但我始终相信,好记性,不如烂笔头。对于我自己而言也是做个开发笔记,感谢您的阅读!
参考文章:guolin.blog.csdn.net/article/det…