Kotlin 协程基础系列:
Kotlin 协程基础一 —— 总体知识概述
Kotlin 协程基础二 —— 结构化并发(一)
Kotlin 协程基础三 —— 结构化并发(二)
Kotlin 协程基础四 —— CoroutineScope 与 CoroutineContext
Kotlin 协程基础五 —— Channel
Kotlin 协程基础六 —— Flow
Kotlin 协程基础七 —— Flow 操作符(一)
Kotlin 协程基础八 —— Flow 操作符(二)
Kotlin 协程基础九 —— SharedFlow 与 StateFlow
Kotlin 协程基础十 —— 协作、互斥锁与共享变量
本篇是 Flow 操作符的第二篇文章,会讲解异常相关操作符、流程监听操作符、flowOn、buffer 操作符、Flow 合并操作符以及将 Flow 转换为其他类型的操作符。
在正式开讲与异常相关的操作符之前,我们有必要再次对异常管理的观点进行阐述:所谓的“异常管理”,在大部分情况下,管理的都是已知异常。对未知异常的管理通常是交给 UncaughtExceptionHandler 来做一个“兜底”处理。
而所谓的已知异常,不是“我知道这里一定会发生异常”,一定会发生的那就不是异常了。异常本来就是正常流程之外的例外流程。对于一个业务而言,异常是除了完美情况下会走的那条通路之外,还可能会走的别的通路。比如网络请求连接超时会收到 TimeoutException,针对这些异常可以去做专门的处理,让整个大流程在发生异常之后,依然可以“正常地”往下走。“正常地”是指,在发生异常时按照设定好的路线继续执行下去。比如连接超时了那就重试或者给用户报错,或者在重试失败后再给用户报错。正确地处理已知的异常,也可以视为“正常”。
Kotlin 官方似乎并不鼓励在 Flow 中通过 try-catch 来捕获异常,而是推荐使用 catch 操作符。这是因为,try-catch 使用不当影响 Flow 异常的可见性,我们通过一个代码示例来详细说明。
以下是在 Flow 中进行网络请求,但是发生超时异常的伪代码:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
emit(1)
emit(2)
emit(3)
}
val job = scope.launch {
flow.collect {
// 如果网络连接超时,有可能抛出 TimeoutException
gitHub.contributors("square", "retrofit")
}
}
job.join()
}
如果只针对某一条数据的异常进行处理,可以在 collect() 内添加 try-catch:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
emit(1)
emit(2)
emit(3)
}
val job = scope.launch {
flow.collect {
// 对可能发生异常的代码 try-catch,这属于是对 Flow 中的每一条数据进行检查
val contributors = try {
gitHub.contributors("square", "retrofit")
} catch (e: TimeoutException) {
// 我们用返回字符串模拟异常处理过程
"Handle Network error"
}
println("Contributors: $contributors")
}
}
job.join()
}
也可以对整个 collect() 加 try-catch:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
emit(1)
emit(2)
emit(3)
}
val job = scope.launch {
// 对整个 collect() try-catch,进行整体检查
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
println("Contributors: $contributors")
}
} catch (e: TimeoutException) {
// 模拟异常处理,实际中可以进行重试或者通知用户
println("Handle Network error")
}
}
job.join()
}
上述两种情况,在发生 TimeoutException 时都会被 catch 捕获,进行异常处理,到这里暂时还没有问题。
现在,假如上游生产数据的过程会使用到数据库或者网络,要对这些代码加上 try-catch,“顺便”包上了 emit():
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
try {
for (i in 1..5) {
// 生产数据的过程可能包含读数据库、网络请求等操作,因此需要加 try-catch,
// 只有正常获取数据之后,才能通过 emit() 在最后发送这些数据
emit(i)
}
} catch (e: TimeoutException) {
println("Error in flow(): $e")
}
}
val job = scope.launch {
try {
flow.collect {
// 对可能发生异常的代码 try-catch,这属于是对 Flow 中的每一条数据进行检查
val contributors = gitHub.contributors("square", "retrofit")
throw TimeoutException()
}
} catch (e: TimeoutException) {
// 我们用返回字符串模拟异常处理过程
println("Handle Network error")
}
}
job.join()
}
运行结果:
Error in flow(): java.util.concurrent.TimeoutException
可以看到,下游抛出的异常,却被上游被捕获了,这不是一个合理的结果。
追溯源码来查看原因,collect() 后面跟着的 lambda 表达式的代码并不是 collect() 的函数体,而是 collect() 的参数 FlowCollector 接口函数 emit() 的内容:
public interface Flow<out T> {
public suspend fun collect(collector: FlowCollector<T>)
}
public fun interface FlowCollector<in T> {
public suspend fun emit(value: T)
}
完整形式相当于:
flow.collect(object : FlowCollector<Int> {
override suspend fun emit(value: Int) {
// collect 后 lambda 表达式的内容
}
})
而这个内容正是上游发送数据调用 emit() 时被执行的。所以当上游在 flow 内添加了 try-catch 进行异常捕获时,如果发生了异常,上游的 try-catch 会先于下游在 collect() 之外的 try-catch 捕获到异常。
这样就会产生一个问题:上游的 try-catch 虽然本意是要捕获数据获取过程中的异常,但是由于它包住了 emit(),所以顺便也把下游的数据消费过程的异常也给拦截了。本该由下游捕获的异常被上游拦截了,假如上游和下游的代码是两个程序员写的,那么下游发生异常的信息没有在预期的位置抛出,不仅给调试带来了困难,同时也会因为上游并不关注下游的异常而忽视这个问题,埋下隐患。
协程里有一个 Exception Transparency 的概念,译为异常可见性,说的是上游的 Flow 不应该吞掉下游的异常。因此,上游在使用 try-catch 时,不应该包住 emit():
val flow = flow {
for (i in 1..5) {
// 让 try-catch 只包含获取数据操作,而不包住 emit()
try {
// 生产数据的过程可能包含读数据库、网络请求等操作...
} catch (e: TimeoutException) {
println("Error in flow(): $e")
throw e
}
emit(i)
}
}
或者使用原来的方式,但是在 catch 中将捕获到的异常原封不动的抛出,以便让下游可以捕获到这个异常:
val flow = flow {
try {
for (i in 1..5) {
emit(i)
}
} catch (e: TimeoutException) {
println("Error in flow(): $e")
// 将原异常继续抛出,让下游捕获
throw e
}
}
两种做法都是为了保证异常的可见性,即为了保证开发者可以拿到他认为可以拿到的异常。
保证异常的可见性只是 Kotlin 对开发者提出的建议,因为保证异常可见性对开发者是有利的。但是遵守这个建议要靠开发者自己,虽然不遵守程序也能运行,但是开发者自己会不方便。
现在进行一下延伸,假如在 Flow 创建后,调用 collect() 之前,使用一些中间操作符,比如 map,考虑两个问题:
捋一下代码流程:
看一下 map() 的源码:
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}
map() 参数上的转换函数 transform() 对其参数 value 进行转换后的结果会通过 emit() 发送到下游,而等号右侧的 transform() 实际上是 kotlinx.coroutines.flow.unsafeTransform
通过 as 重命名后的函数,其参数是一个接收者类型为 FlowCollector 的挂起函数。所以 transform {} 内的有一个 FlowCollector 类型的 this 使得 map() 可以直接调用 emit() 将数据发送到下游。
实际上,emit() 的内容就是其下游的 collect 后面的 lambda 表达式的内容。因此 collect 后的代码块,也就是 lambda 表达式如果抛出异常,会先通过 emit() 传到 map() 中。由于 map() 内没有进行异常处理,因此异常会继续向上游 Flow 传递。
至此,两个问题也有了答案,下游抛出的异常会经过 map 函数(但注意不是 map 后面接的代码块,代码块指定的是参数上的 transform 如果对数据进行转换,而异常是从 map 内部调用的 emit() 抛出的),map 抛出的异常会向上游的 Flow 继续抛出。
假如在中间加一个 transform 操作符,由于 transform 内部也是通过显示调用 emit() 将数据发送到下游,假如对 transform 的 emit() 也加了 try-catch,那么与前面例子中的头部 Flow 一样,它也会拦截下游抛出的异常。因此,我们不能只关注起始的 Flow 中的 emit(),对于其他通过显式调用 emit() 发送数据的操作符,也要注意不要对 emit() 加 try-catch。
总之,结论就一句话,别用 try-catch 包住任何一个 emit()。
catch() 的作用,相当于在 flow {…} 范围内加了 try-catch,但是捕获 flow {…} 内除了 emit() 以外代码抛出的异常。即 catch() 只会捕获上游异常,而不会捕获下游异常(包括所有代码块与各种操作符)。
catch() 也不会捕获 CancellationException,因为这个异常就是用来取消协程的。
先看示例代码:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
emit(i)
}
}.catch { println("catch(): $it") }
val job = scope.launch {
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
throw TimeoutException()
}
} catch (e: TimeoutException) {
println("Handle Network error")
}
}
job.join()
}
运行结果:
Handle Network error
通过结果能看出 TimeoutException 是被下游捕获的,而不是 catch() 捕获的。假如,我在上游获取数据时发生了异常:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
// 模拟获取数据时发生的异常
throw RuntimeException("flow() error")
emit(i)
}
}.catch { println("catch(): $it") }
val job = scope.launch {
try {
flow.collect {
val contributors = gitHub.contributors("square", "retrofit")
throw TimeoutException()
}
} catch (e: TimeoutException) {
println("Handle Network error")
}
}
job.join()
}
那么就只会由 catch() 捕获到这个上游异常:
catch(): java.lang.RuntimeException: flow() error
因为还没到 emit() 发送数据到下游这一步呢,就发生异常并被 catch() 捕获了。
如果有多个 catch(),那么每个 catch() 拿到的就是它上游的异常:第一个 catch() 拿到的是它到起始 Flow 之间发生的异常;第二个 catch() 拿到的是与第一个 catch() 之间发生的异常,以此类推……
什么时候用 catch()?如何在 catch() 与 try-catch 之间做选择?
二者有一个关键区别就是 try-catch 是在 Flow 里面工作的,而 catch() 是在 Flow 之后工作的。这就导致了,如果 Flow 内部发生异常,可以通过 try-catch 直接进行修复,让 Flow “活着”继续进行生产工作;但 catch() 就只能在 Flow 之后接管数据生产操作,因为只有 Flow 内部的异常没有被处理,而是被抛出了,才能流到 catch():
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
for (i in 1..5) {
// 假设在 i = 3 时生产数据过程发生了异常,在 Flow 内部可以通过 try-catch 修复
if (i == 3) {
try {
throw RuntimeException("flow() error")
} catch (e: Exception) {
emit(i)
}
} else {
emit(i)
}
}
}
val job = scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: RuntimeException) {
// 我们用返回字符串模拟异常处理过程
println("Error")
}
}
job.join()
}
假如在 Flow 内部通过 try-catch 修复异常,仍有可能会收到完整的数据:
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5
但假如由于权限问题,我们不能修改 Flow 内部的代码,那么就只能通过 Flow 外接 catch() 来尝试接管数据生产流程:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
// 模拟上游发送数据
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow() error")
} else {
emit(i)
}
}
}.catch {
println("flow() error")
emit(100)
emit(200)
emit(300)
}
val job = scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: RuntimeException) {
// 我们用返回字符串模拟异常处理过程
println("Error")
}
}
job.join()
}
这样下游能得到一部分原有数据,以及 catch() 接管生产的数据:
Data: 1
Data: 2
flow() error
Data: 100
Data: 200
Data: 300
因此,在二者选择的问题上,优先选择 try-catch,如果因为 Flow 的代码结构问题而无法选择 try-catch,才使用 catch()。结构问题具体是指没有权限修改 Flow 内部代码时,只能在 Flow 外接 catch() 来做一个无奈之下的接管。
但实际情况往往是,catch() 没有办法完美接管上游生产数据的逻辑,所以在 catch() 中通常只能做一些收尾工作。无缝、完美的接管通常是做不到的。
retry() 与 retryWhen() 也是针对上游 Flow 的异常的,核心原理与 catch() 相同。区别在于,retry() 与 retryWhen() 在异常时重启上游 Flow。
示例代码:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow() error")
} else {
emit(i)
}
}
}.map { it * 2 }.retry(2) { // it:Throwable
it is RuntimeException
}
val job = scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: RuntimeException) {
// 我们用返回字符串模拟异常处理过程
println("Caught RuntimeException!")
}
}
job.join()
}
运行结果:
Data: 2
Data: 4
Data: 2
Data: 4
Data: 2
Data: 4
Caught RuntimeException!
retry() 括号内的参数指的是重试的次数,由于上游在发出 1、2 两条数据后才抛出异常,再加上重试的两次,因此最终会有 3 组数据输出。
此外,retry() {…} 的大括号内指定的是一个 predicate 谓词条件,只有该条件为 true 时 retry() 才会进行重试。如果返回了 false,即使重试次数尚未完成,也不会继续重试,而是将异常向下游抛出。
retryWhen() 相当于是把 retry() 的两个参数合并到一起的版本:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
if (i == 3) {
throw RuntimeException("flow() error")
} else {
emit(i)
}
}
}.map {
it * 2
}.retryWhen { cause, attempt ->
println("Exception cause: ${cause.message}, attempted time: $attempt")
// 返回 Boolean 类型的重试条件,为 true 时才重试
cause is RuntimeException && attempt <= 2
}
val job = scope.launch {
try {
flow.collect {
println("Data: $it")
}
} catch (e: RuntimeException) {
// 我们用返回字符串模拟异常处理过程
println("Caught RuntimeException!")
}
}
job.join()
}
运行结果:
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 0
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 1
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 2
Data: 2
Data: 4
Exception cause: flow() error, attempted time: 3
Caught RuntimeException!
retryWhen() 的两个参数,cause 就是导致异常的 Throwable 对象,而 attempt 是已经尝试过的次数,异常第一次到达 retryWhen() 时,attempt 是 0。由于设置了 attempt <= 2 都可进行重试,因此一共重试三次。
其实前面讲的 catch 与 retry 操作符也属于流程监听操作符,只不过异常的处理比较特殊,所以单列出来。接下来几个操作符是对 Flow 的启动和结束的监听。
onStart() 负责监听 Flow 的收集流程的开始事件,执行时机是在 collect() 被调用之后,在正式开始生产数据之前(调用上游的 collect() 之前)。
示例代码:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
for (i in 1..5) {
emit(i)
}
}.onStart { println("onStart 1") }
.onStart { println("onStart 2") }
val job = scope.launch {
flow.collect {
println("Data: $it")
}
}
job.join()
}
运行结果:
onStart 2
onStart 1
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5
可以看到,onStart() 是在 collect() 之前运行的,并且下面的 onStart() 先于上面的 onStart() 运行。
由于 onStart() 是在 collect() 之前运行的,因此假如在 onStart() 内抛出了异常,在上游 try-catch 是捕获不到的,只能通过 catch() 捕获:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
// onStart() 先于这里的代码执行且抛出异常,因此这部分代码实际上没有得以执行
try {
for (i in 1..5) {
emit(i)
}
} catch (e: Exception) {
e.printStackTrace()
}
}.onStart {
println("onStart 1")
throw RuntimeException("onStart error")
}.onStart {
println("onStart 2")
}.catch {
// 使用 catch() 可以捕获到 onStart() 内抛出的异常
println("catch: $it")
}
val job = scope.launch {
flow.collect {
println("Data: $it")
}
}
job.join()
}
运行结果:
onStart 2
onStart 1
catch: java.lang.RuntimeException: onStart error
由于生产过程还没开始就抛出了异常,因此结果中不会打印任何数据。但是这个由 onStart() 抛出的异常确实被 catch() 捕获到了。
onCompletion() 监听的是 Flow 的结束,在所有数据发送完毕后触发。除了正常结束,异常结束也会触发 onCompletion():
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
try {
for (i in 1..5) {
emit(i)
}
} catch (e: Exception) {
e.printStackTrace()
}
}.onStart {
println("onStart 1")
throw RuntimeException("onStart error")
}.onStart {
println("onStart 2")
}.onCompletion { // it:Throwable? 如果是正常结束,it 就是 null
println("onCompletion $it")
}.catch {
println("catch: $it")
}
val job = scope.launch {
flow.collect {
println("Data: $it")
}
}
job.join()
}
运行结果:
onStart 2
onStart 1
onCompletion java.lang.RuntimeException: onStart error
catch: java.lang.RuntimeException: onStart error
可以看到,异常结束确实触发了 onCompletion() 的执行,并且 onCompletion() 没有拦截异常,catch() 中的内容也打印了。
onEmpty() 监听一条数据都没有的情况,其代码块会在 Flow 正常结束且没有发送过一条数据的时候被触发。一定是正常结束才触发,异常结束不会触发。
flowOn() 用来定制其上游 Flow 运行的 CoroutineContext 的,大多数时候用来切线程,但是也可以切换其他的 CoroutineContext,如 CoroutineName 等。
先关注如何获取 Flow 的 CoroutineContext:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): $coroutineContext")
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..5) {
emit(i)
}
}
val job = scope.launch {
flow.collect {
println("Data: $it")
}
}
job.join()
}
运行结果:
CoroutineContext in flow(): [BlockingCoroutine{Active}@6108c233, BlockingEventLoop@74ab1442]
CoroutineContext in flow(): [StandaloneCoroutine{Active}@19406503, Dispatchers.Default]
Data: 1
Data: 2
Data: 3
Data: 4
Data: 5
flow.collect() 是在 scope 内调用的,而 scope 用的是 EmptyCoroutineContext,因此其协程上下文应该是 Default。这是因为 Flow 在哪个协程调用的 collect(),它的生产流程就在该协程中启动,也就处于该协程的 CoroutineContext 的上下文环境中。因此,在 Flow 中需要使用 currentCoroutineContext() 才能获取到正确的 CoroutineContext。
再看 flowOn() 的效果,只会对其上游进行切换,不会影响到下游:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..5) {
emit(i)
}
}.map {
println("CoroutineContext in map() 1: ${currentCoroutineContext()}")
it * 2
}.flowOn(Dispatchers.IO).map {
println("CoroutineContext in map() 2: ${currentCoroutineContext()}")
it * 2
}
val job = scope.launch {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
job.join()
}
运行结果:
CoroutineContext in flow(): [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 1: [ProducerCoroutine{Active}@a9dc865, Dispatchers.IO]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 4 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 8 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 12 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 16 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
CoroutineContext in map() 2: [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
Data: 20 - [ScopeCoroutine{Active}@5553375d, Dispatchers.Default]
可以看到 flowOn() 的上游都被切换为 Dispatchers.IO,而下游还都在 Dispatchers.Default 下。
flowOn() 只切换上游的 CoroutineContext,是因为上下游的代码有可能是两个程序员写的。下游通常不会关注上游写了什么,因此如果 flowOn() 连下游的 CoroutineContext 都能切换,会让下游的代码行为变得难以(被只写下游代码的程序员)预期。
看到 flowOn() 切换线程,很容易想起 withContext()。那么,在 Flow 中可以使用 withContext() 切换协程上下文吗?答案是可以,但是要注意使用方式:
fun main() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
// 使用 withContext() 切换 Flow 的协程上下文
withContext(Dispatchers.IO) {
for (i in 1..5) {
emit(i)
}
}
}.map {
println("CoroutineContext in map() 1: ${currentCoroutineContext()}")
it * 2
}
val job = scope.launch {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
job.join()
}
像这样直接包住 emit() 是不被允许的,运行会抛出如下异常:
CoroutineContext in flow(): [StandaloneCoroutine{Active}@1b4c838, Dispatchers.Default]
Exception in thread "DefaultDispatcher-worker-3" java.lang.RuntimeException: Exception while trying to handle coroutine exception
...
Suppressed: java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [StandaloneCoroutine{Active}@1b4c838, Dispatchers.Default],
but emission happened in [DispatchedCoroutine{Active}@32f9a3f6, Dispatchers.IO].
Please refer to 'flow' documentation or use 'flowOn' instead
意思是违反了 Flow 的不变性,实际是指手动定制了 emit() 在哪个协程中运行,这是违规的。因此 withContext() 的使用与 try-catch 类似,不能包住 emit()。
总结下来就是,使用哪个函数进行切换,取决于切换范围的大小。如果要对 flow 的代码块或者某个操作符代码块的内部的一部分代码进行切换,那就只能用 withContext(),因为 flowOn() 达不到这么细的粒度;如果想对一整个操作符,或者连续的几个操作符做切换,最好用 flowOn(),因为用 withContext() 需要单独对每一个操作符进行设置,还要刻意把代码拆开避免包住 emit(),太麻烦。
多个 flowOn() 的情况,每个 flowOn() 就只控制它上游到上一个 flowOn() 之间的部分的协程上下文;第一个 flowOn() 控制的就是从 flow() 到 flowOn() 这一部分。
而下游调用 flow.collect() 这部分的上下文,有多种控制办法,可以直接使用 flow.collect() 所在的协程的上下文:
// 直接使用 launch 指定的协程上下文
val job = scope.launch(Dispatchers.IO) {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
也可以在协程内使用 withContext() 进行切换:
val job = scope.launch {
// 用 withContext 切换上下文
withContext(Dispatchers.IO) {
flow.collect {
println("Data: $it - ${currentCoroutineContext()}")
}
}
}
还有一种 Kotlin 官方推荐的方式,将 collect() 内原本的代码放到 onEach() 中,然后用 flowOn() 进行切换:
val job = scope.launch {
flow.onEach {
println("Data: $it - ${currentCoroutineContext()}")
}.flowOn(Dispatchers.IO).collect {
}
}
官方还推荐了另一种使用 launchIn() 的写法:
val job = scope.launch {
flow.onEach {
println("Data: $it - ${currentCoroutineContext()}")
}.launchIn(Dispatchers.IO).collect {
}
}
与 flowOn() 相关的还有一个底层概念叫做 fuse,可以翻译为融合或合并。意思是如果连续调用多个 flowOn(),最终只会由第一个 flowOn() 创建一个新的 Flow 对象,后续的 flowOn() 不会创建新的 Flow 对象,而是把上下文参数与前面的上下文进行相加:
private fun fuse() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..3) {
emit(i)
}
}.flowOn(Dispatchers.IO).flowOn(CoroutineName("Test")).flowOn(Dispatchers.Default)
val job = scope.launch {
flow.onEach {
println("Data: $it - ${currentCoroutineContext()}")
}.collect {}
}
job.join()
}
运行结果:
CoroutineContext in flow(): [CoroutineName(Test), ProducerCoroutine{Active}@126bd623, Dispatchers.IO]
Data: 1 - [ScopeCoroutine{Active}@34cd5a8, Dispatchers.Default]
Data: 2 - [ScopeCoroutine{Active}@34cd5a8, Dispatchers.Default]
Data: 3 - [ScopeCoroutine{Active}@34cd5a8, Dispatchers.Default]
上游的 CoroutineContext 有指定名字的 CoroutineName,还有指定的 Continuation —— Dispatchers.IO,这是因为 flowOn() 融合的时候,是从右往左加。因此如果有相同类型的 CoroutineContext,左边的值会被留下,右边的被覆盖掉。这与通过 plus(),也就是 +
的覆盖规则相反。
flowOn() 除了可以与相邻的 flowOn() 融合,还可以与 channelFlow() 融合。flowOn() 其实是通过 Channel 来切换 CoroutineContext 的,更确切地说,它与 channelFlow() 在底层用的是同一套实现 —— ChannelFlow。所以,当 flowOn() 与 channelFlow() 连用时,flowOn() 不会创建新的 Flow,而是与 channelFlow() 所提供的 Flow 对象融合:
private fun fuseWithChannelFlow() = runBlocking {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = channelFlow {
println("CoroutineContext in flow(): ${currentCoroutineContext()}")
for (i in 1..3) {
send(i)
}
}.flowOn(Dispatchers.IO)
val job = scope.launch {
flow.collect {}
}
job.join()
}
运行结果表明 channelFlow() 内的代码运行在 Dispatchers.IO 上:
CoroutineContext in flow(): [ProducerCoroutine{Active}@6dab9d66, Dispatchers.IO]
buffer() 会给 Flow 加上缓冲功能。
Flow 是线性逻辑,有两个维度:
假如想将这种线性结构改为并行结构,也要从两个维度考虑:
对于第二点,我们举一个代码示例:
private fun flowOnSample() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val start = System.currentTimeMillis()
val flow = flow {
for (i in 1..5) {
emit(i)
println("Emitted: $i - ${System.currentTimeMillis() - start}ms")
}
}.flowOn(Dispatchers.IO) // flowOn() 将数据生产切换到 IO 线程中
val job = scope.launch {
flow.collect {
// 演示用于模拟下游处理数据较慢的情形
delay(1000)
println("Data: $it - ${System.currentTimeMillis() - start}ms")
}
}
job.join()
}
运行结果:
Emitted: 1 - 37ms
Emitted: 2 - 49ms
Emitted: 3 - 50ms
Emitted: 4 - 50ms
Emitted: 5 - 50ms
Data: 1 - 1049ms
Data: 2 - 2063ms
Data: 3 - 3068ms
Data: 4 - 4083ms
Data: 5 - 5097ms
可以看到上游生产数据几乎一瞬间就全都完成了,而下游接收数据时,由于增加了模拟数据处理的延时,因此要慢上很多。但重要的是,通过 flowOn() 切换上游协程环境,实现了多条数据之间的并行处理(数据“一起”发出,而不是处理完一条发出下一条)。
flowOn() 底层通过 Channel 实现协程切换,Channel 要想实现这种功能,除了切协程之外,还需要数据缓冲,也就是 buffer 功能。也就是说,不仅要把上游生产流程和下游处理流程放在不同的协程中,还需要暂存上游生产好的数据。这个缓冲功能在 flowOn() 中是默认打开的,具体就是由支持缓冲的 Channel 实现的,只不过 flowOn() 不支持对缓冲的配置,buffer() 才支持。
buffer() 与 flowOn() 和 channelFlow() 一样,底层都是由 ChannelFlow 实现的。也就是说,你用 buffer() 和 flowOn() 创建的都是 ChannelFlow,只不过对其进行了不同的配置:
// 最终创建 ChannelFlowOperatorImpl 传入的参数不同
public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T> {
checkFlowContext(context)
// flowOn() 传入 context
return when {
context == EmptyCoroutineContext -> this
this is FusibleFlow -> fuse(context = context)
else -> ChannelFlowOperatorImpl(this, context = context)
}
}
// capacity 是缓冲区大小,onBufferOverflow 是缓冲区溢出时的处理策略,与 Channel 的参数完全相同
@Suppress("NAME_SHADOWING")
public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow<T> {
...
// desugar CONFLATED capacity to (0, DROP_OLDEST)
var capacity = capacity
var onBufferOverflow = onBufferOverflow
if (capacity == CONFLATED) {
capacity = 0
onBufferOverflow = BufferOverflow.DROP_OLDEST
}
// buffer 传入 capacity 和 onBufferOverflow
// create a flow
return when (this) {
is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow)
else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow)
}
}
想配置 CoroutineContext 就使用 flowOn(),想配置缓冲就使用 buffer()。
由于 buffer() 与 flowOn() 和 channelFlow() 一样,底层都是由 ChannelFlow 实现的,因此 flowOn() 与 channelFlow() 之间的融合特性也适用于 buffer()。
buffer() 可以脱离 flowOn() 单独使用,它会开启对上游 Flow 的缓冲,内部实际上是通过创建 ChannelFlow 通过协程切换的方式实现了缓冲效果。因此,buffer() 实际上会帮你切协程。反之,由于 flowOn() 默认开启了缓冲功能,所以通过 flowOn() 创建的 Flow 也会具有缓冲功能。
多个 buffer() 进行融合时,由于 buffer() 上有两个参数,因此要比 flowOn() 的融合规则复杂一点。
先看 onBufferOverflow 这个参数的维度,它有三个值可选,默认值是 SUSPEND:
public enum class BufferOverflow {
SUSPEND,
DROP_OLDEST,
DROP_LATEST
}
假如右侧的 buffer() 使用了非 SUSPEND,那么右侧的 buffer() 就会完全覆盖左侧的 buffer():
/**
* 右侧 buffer() 使用了非 SUSPEND 的溢出策略,会完全覆盖左侧的
*/
private fun fuse1() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val start = System.currentTimeMillis()
val flow = flow {
for (i in 1..5) {
emit(i)
println("Emitted: $i - ${System.currentTimeMillis() - start}ms")
}
}.buffer(10, BufferOverflow.DROP_LATEST)
.flowOn(Dispatchers.IO)
.buffer(2, BufferOverflow.DROP_OLDEST)
val job = scope.launch {
flow.collect {
// 演示用于模拟下游处理数据较慢的情形
delay(1000)
println("Data: $it - ${System.currentTimeMillis() - start}ms")
}
}
job.join()
}
运行结果:
Emitted: 1 - 43ms
Emitted: 2 - 58ms
Emitted: 3 - 58ms
Emitted: 4 - 58ms
Emitted: 5 - 59ms
Data: 1 - 1058ms
Data: 4 - 2068ms
Data: 5 - 3080ms
下游只收到 3 条数据 1、4、5,这是因为执行了右侧 buffer() 的结果:
假如右侧 buffer() 没有写 onBufferOverflow 这个参数,即使用默认值 SUSPEND,或者直接显式写了 SUSPEND,那么融合时,会采用左侧 buffer 的 onBufferOverflow,并且将两个 buffer() 的 capacity 进行融合,具体的融合策略是:
buffer() 有一个变种的便捷函数 conflate(),相当于是只缓冲最新的一条数据的 buffer(),效果与 buffer(CONFLATED)
一样。
前面我们说到 collect() 有一个变种 collectLatest():
public suspend fun <T> Flow<T>.collectLatest(action: suspend (value: T) -> Unit) {
mapLatest(action).buffer(0).collect()
}
mapLatest() 是一个可以随时被新数据取消当前数据的转换流程的、增强版的 map()。它实际上隐含着一个功能,就是在当前数据处理过程中,下一条数据就可以生产了,否则如果下一条数据无法生产也就无法打断当前数据的转换流程了。因此 mapLatest() 也是有多协程支持的,这个支持也是由 Channel 在底层实现的,与 buffer() 和 flowOn() 用的是相同的底层支持,所以 mapLatest() 也可以和 buffer() 以及 flowOn() 融合。
默认情况下,mapLatest() 是有缓冲的,后面接上 buffer(0) 会将缓冲关闭。关闭缓冲后,假如上游是异步生产数据的,且生产速度要大于下游的处理速度,那么由于上一条数据的 collect() 还没执行完,因此上游生产的数据就会在 mapLatest(action).buffer(0)
这个位置挂起,此时如果上游继续发射数据,数据到 mapLatest() 时就会冲掉老的数据(因为缓冲被关闭了),从而实现只有最新数据能被下游处理到的功能:
private fun collectLatestSample() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val start = System.currentTimeMillis()
val flow = flow {
for (i in 1..5) {
emit(i)
println("Emitted: $i - ${System.currentTimeMillis() - start}ms")
}
}.flowOn(Dispatchers.IO).mapLatest { it }.buffer(0)
val job = scope.launch {
flow.collect {
// 演示用于模拟下游处理数据较慢的情形
delay(1000)
println("Data: $it - ${System.currentTimeMillis() - start}ms")
}
}
job.join()
}
运行结果:
Emitted: 1 - 39ms
Emitted: 2 - 50ms
Emitted: 3 - 50ms
Emitted: 4 - 50ms
Emitted: 5 - 50ms
Data: 1 - 1045ms
Data: 5 - 2050ms
第一条数据顺利进入下游处理,由于处理时间较长(加了 1s 的模拟延时),因此 2、3、4 这三条数据在到达 mapLatest() 后会被卡柱,上游生产的新数据也到达 mapLatest() 后,由于其缓冲已被关闭,所以前面的数据会被丢掉,只有最后一条数据 5 因为没有后续数据被生产出来得以保存,待延时结束数据 1 被下游处理后,5 会流到下游得以处理,因此下游处理的数据只有 1 和 5。
由于 collectLatest() 内的 collect() 代码块是空的,因此其数据处理过程是瞬间就完成的,这就导致它的缓冲 buffer(0) 根本就排不上用场。换句话说,buffer(0) 写不写没有什么实质性区别,官方注释中倒是对这个做了解释,说 buffer(0) 可以确保 mapLatest() 的代码块一定会按照数据生产的线性顺序去执行。
因此 mapLatest(action).buffer(0)
的效果就是一个“下游处理完成之前,上游就不再转换新数据”的 mapLatest(),再加一个空的 collect() 就构成了 collectLatest()。
使用 merge() 可以将多个 Flow 合并:
private fun mergeSample() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf(4, 5, 6)
val mergedFlow = merge(flow1, flow2)
val job = scope.launch {
mergedFlow.collect {
println("$it")
}
}
job.join()
}
运行结果:
1
2
3
4
5
6
merge() 会按照每条数据的发送时间来转发数据:
private fun mergeSample() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flow {
delay(100)
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
val flow2 = flow {
delay(50)
emit(4)
delay(100)
emit(5)
delay(100)
emit(6)
}
val mergedFlow = merge(flow1, flow2)
val job = scope.launch {
mergedFlow.collect {
println("$it")
}
}
job.join()
}
运行结果:
4
1
5
2
6
3
也可以调用 Iterable 的 merge() 得到一个合并后的 Flow:
private fun mergeSample4() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf(4, 5, 6)
val flowList = listOf(flow1, flow2)
val mergedFlowFromList = flowList.merge()
val job = scope.launch {
mergedFlowFromList.collect {
println("$it")
}
}
job.join()
}
运行结果:
1
2
3
4
5
6
如果数据类型不是多个独立的 Flow,也不是 Iterable,而是 Flow 的 Flow —— Flow
,它的多条数据之间也可以合并,通常这种操作叫做 flatten,译为展开或铺平。由于这种数据的 Flow 不是现成的,而是一个一个生产出来的,所以展开有两种方式:顺序展开与穿插式展开。
顺序展开用的是 flattenConcat():
@OptIn(ExperimentalCoroutinesApi::class)
private fun flattenConcatSample() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf(4, 5, 6)
val flowFlow = flowOf(flow1, flow2)
// concat 是 concatenate 的缩写
// flattenConcat 是 ExperimentalCoroutinesApi
val concatenatedFlowFlow = flowFlow.flattenConcat()
val job = scope.launch {
// 按序输出 1 ~ 6
concatenatedFlowFlow.collect {
println("$it")
}
}
job.join()
}
在一个 flow1 元素发送出来之后,直接用挂起协程的形式把生产 Flow 的 Flow,即 flowFlow 给暂停,先收集 flow1 的所有元素,把它发送的每条数据转发出去,然后才生产下一个 flow2。
而 flattenMerge() 则是穿插式的展开,允许在第一个 Flow 没有完成数据发射与收集过程的情况下,进行其他 Flow 的数据发射与收集:
@OptIn(ExperimentalCoroutinesApi::class)
private fun flattenMerge() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
val flow2 = flowOf(4, 5, 6)
val flowFlow = flowOf(flow1, flow2)
val mergedFlowFlow = flowFlow.flattenMerge()
val job = scope.launch {
mergedFlowFlow.collect {
println("$it")
}
}
job.join()
}
运行结果:
1
4
5
6
2
3
协程还提供了将普通的 Flow 先转换成 Flow 的 Flow,然后再展开的操作符。在介绍它们之前,我们先用之前介绍过的操作符来完成上述事项:
@OptIn(ExperimentalCoroutinesApi::class)
private fun flatMapDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
// 构造一个生产 Flow 的 Flow,通过 X - Y 这种形式声明 Y 这个数据是由 X 而来的
val mappedFlow = flow1.map { from -> (1..from).asFlow().map { "$from - $it" } }
val concatenatedMappedFlow = mappedFlow.flattenConcat()
val job = scope.launch {
concatenatedMappedFlow.collect {
println(it)
}
}
job.join()
}
运行结果:
1 - 1
2 - 1
2 - 2
3 - 1
3 - 2
3 - 3
使用 flatMapConcat() 可以直接实现 map() 与 flattenConcat() 连用的效果:
@OptIn(ExperimentalCoroutinesApi::class)
private fun flatMapDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
// flatMapConcat 相当于 map 与 flattenConcat 连用
val concatenatedMappedFlow = flow1.flatMapConcat { from ->
(1..from).asFlow().map { "$from - $it" }
}
val job = scope.launch {
concatenatedMappedFlow.collect {
println(it)
}
}
job.join()
}
类似的,还有 flatMapMerge() 相当于将 map() 与 flattenMerge() 连环使用,即先转换,然后同时收集,互相穿插式的展开:
@OptIn(ExperimentalCoroutinesApi::class)
private fun flatMapDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1, 2, 3)
val mergedMappedFlow = flow1.flatMapMerge { from ->
(1..from).asFlow().map { "$from - $it" }
}
val job = scope.launch {
mergedMappedFlow.collect {
println(it)
}
}
job.join()
}
运行结果:
1 - 1
3 - 1
3 - 2
3 - 3
2 - 1
2 - 2
flatMapLatest() 与 flatMapConcat() 类似,都是按顺序展开,但区别在于 “Latest” 不会在进行当前 Flow 的收集和转发时卡住上游 Flow 的生产,而是上游继续生产,一旦下一个 Flow 生产出来就终止当前 Flow 的收集,开始收集和转发这个刚生产出来的 Flow。
展开只是对多个 Flow 进行合并的一种形式,它把多个 Flow 的数据放在一个统一的 Flow 里发送。
combine() 可以把两个 Flow 的数据进行结合计算,输出计算结果作为新 Flow 的数据:
private fun combineDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flow {
delay(100)
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
val flow2 = flow {
delay(50)
emit(4)
delay(100)
emit(5)
delay(100)
emit(6)
}
// combine() 两种方式的效果相同,只不过第二种可以接收多个 Flow 作为参数
val combinedFlow1 = flow1.combine(flow2) { a, b -> "$a - $b" }
val combinedFlow2 = combine(flow1, flow2, flow1) { a, b, c -> "$a - $b - $c" }
val job = scope.launch {
combinedFlow1.collect { println(it) }
println("======")
combinedFlow2.collect { println(it) }
}
job.join()
}
运行结果:
1 - 4
1 - 5
2 - 5
2 - 6
3 - 6
======
1 - 4 - 1
1 - 5 - 1
2 - 5 - 2
2 - 6 - 2
3 - 6 - 3
对第一组结果稍作解释:
zip() 与 combine() 类似,只不过用过的数据,zip() 就不再使用了:
private fun combineDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flow {
delay(100)
emit(1)
delay(100)
emit(2)
delay(100)
emit(3)
}
val flow2 = flow {
delay(50)
emit(4)
delay(100)
emit(5)
delay(100)
emit(6)
}
val zippedFlow = flow1.zip(flow2) { a, b -> "$a - $b" }
val job = scope.launch {
zippedFlow.collect { println(it) }
}
job.join()
}
运行结果:
1 - 4
2 - 5
3 - 6
我们对 zip 这个词的理解大多数时候为“压缩”,但实际上,zip 更常用的含义为“拉链”。zip() 也是对拉链这个含义的具象化表示,它会将两个 Flow 当做拉链的左右两侧,使得数据一一对应。
最后要介绍 combineTransform(),它与 combine() 的区别就像 transform() 与 map() 的区别一样。它不是让你把结合完的数据提供出来,而是让你自己发送数据:
val combinedFlow = flow1.combine(flow2) { a, b -> "$a - $b" }
val combinedTransform = flow1.combineTransform(flow2) { a, b -> emit("$a - $b") }
好处是可以不局限于一对一的限制了,一对或者一组来自上游的数据,可以发送一条下游的数据,也可以不发送或发送多条下游数据(总之与 transform() 相对于 map() 的好处一样)。
在收集 Flow 的过程里做各种整理计算,把结果归结成返回值输出。
first() 会调用 Flow 的 collect() 开始收集数据,在第一条数据发送出来之后,直接结束收集,把这条数据返回:
private fun firstDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
val job = scope.launch {
// 1.返回整个 Flow 的第一个元素
println("First: ${flow.first()}")
// 2.返回符合条件的第一个元素
println("First with condition: ${flow.first { it > 2 }}")
// 3.空的 Flow 调用 first() 会抛出 NoSuchElementException
val emptyFlow = flowOf<Int>()
try {
emptyFlow.first()
} catch (e: NoSuchElementException) {
println("No element.")
}
// 4.使用 firstOrNull() 在找不到元素时会返回 null
println("First of empty flow: ${emptyFlow.firstOrNull()}")
println("First of flow: ${flow.firstOrNull { it > 5 }}")
}
job.join()
}
运行结果:
First: 1
First with condition: 3
No element.
First of empty flow: null
First of flow: null
last 操作符与 first 类似,不多说,直接说下一个操作符 single。
single 是一个终端操作符,等待仅有一个值被发射的 Flow。对于空的流会抛出 NoSuchElementException 异常,对于包含多个元素的流会抛出 IllegalArgumentException 异常:
private fun singleDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow1 = flowOf(1)
val flow2 = flowOf(1, 2)
val job = scope.launch {
// single 只能用于仅有一个数据的 Flow
println("single element1: ${flow1.single()}")
// 如果 Flow 多于 1 条数据,single 会抛出 IllegalArgumentException
try {
println("single element2: ${flow2.single()}")
} catch (e: IllegalArgumentException) {
println("Flow has more than one element")
}
// 使用 singleOrNull 会返回 null 避免抛出异常
println("single element2: ${flow2.singleOrNull()}")
}
job.join()
}
运行结果:
single element1: 1
Flow has more than one element
single element2: null
count() 用于计算 Flow 中元素的数量。它会返回一个表示 Flow 中元素数量的整数值:
private fun countDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
val job = scope.launch {
// 1.默认计算所有元素数:5
println("Flow count: ${flow.count()}")
// 2.计算满足指定条件的元素数量:3
println("Flow odd count: ${flow.count { it % 2 != 0 }}")
}
job.join()
}
将 Flow 转换成对应的集合类型:
private fun toIterableDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
val job = scope.launch {
println("toList: ${flow.toList()}")
println("toSet: ${flow.toSet()}")
println("toCollection: ${flow.toCollection(LinkedHashSet())}")
}
job.join()
}
注意事项:
produceIn() 用于将 Flow 发送的元素生产到指定的通道(Channel)中。这个操作符可以帮助将 Flow 中的元素发送到通道,以便其他协程可以从通道接收这些元素:
private fun produceInDemo() = runBlocking<Unit> {
val scope = CoroutineScope(EmptyCoroutineContext)
val flow = flowOf(1, 2, 3, 4, 5)
val job = scope.launch {
val channel = flow.produceIn(this)
for (element in channel) {
println("Received element: $element")
}
}
job.join()
}
运行结果:
Received element: 1
Received element: 2
Received element: 3
Received element: 4
Received element: 5