Flow基础

126 阅读14分钟

本文翻译自官方文档。

原英文链接:kotlinlang.org/docs/flow.h…

原中文链接:book.kotlincn.net/text/flow.h…

异步流

在协程中挂起函数可以异步返回单个值,但如果我们想返回多个异步计算出来的值该如何解决呢,这时候就要用到 flow。

表示多个值

在 Kotlin 中可以使用集合来表示多个值。下面的 simple 函数会返回 3 个数字的列表,然后我们可以在 main 函数中通过列表的 forEach 函数来打印这 3 个数字:

fun simple(): List<Int> = listOf(1, 2, 3)
 
fun main() {
    simple().forEach { value -> println(value) } 
}

序列(Sequence)

如果计算出数值需要使用耗 CPU 会阻塞线程的代码(每次计算耗费 100 ms),那么我们可以用 Sequence 来表示这些数值:

fun simple(): Sequence<Int> = sequence { // sequence builder
    for (i in 1..3) {
        Thread.sleep(100) // pretend we are computing it
        yield(i) // yield next value
    }
}

fun main() {
    simple().forEach { value -> println(value) } 
}

打印结果跟上面一样,只不过每次打印前会等待 100 ms。

挂起函数

然而上面的代码会阻塞主线程,当使用异步的代码计算这些值的时候我们可以使用 suspend 来修饰 simple 函数,这样就不会阻塞线程:

suspend fun simple(): List<Int> {
    delay(1000) // pretend we are doing something asynchronous here
    return listOf(1, 2, 3)
}

fun main() = runBlocking<Unit> {
    simple().forEach { value -> println(value) } 
}

这段代码等待 1 秒后打印数字。

Flow

使用 List<Int> 意味着只能一次返回所有的值,想要返回异步计算的多个值可以使用 Flow<Int> :

fun simple(): Flow<Int> = flow { // flow builder:flow 构建器
    for (i in 1..3) {
        delay(100) // pretend we are doing something useful here
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> {
    // Launch a concurrent coroutine to check if the main thread is blocked
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    // Collect the flow
    simple().collect { value -> println(value) } 
}

打印如下:

I'm not blocked 1 
1 
I'm not blocked 2 
2 
I'm not blocked 3 
3

这段代码在 runBlocking{ ... } 代码块中启动了一个协程,该协程运行在主线程。从“ I'm not blocked ” 和 数字 交叉打印可以看出, 这段代码不会阻塞主线程。

flow 的代码如下:

public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)

可以看到其参数 block 是一个挂起函数,所以可以在这里调用 delay() 方法,其返回值为 Flow<Int>。

这里有两个新的函数:emit 函数和 collect 函数,Flow 使用 emit 函数 发射值,使用 collect 函数 收集值

你可以试试在 simple 的 flow { ... } 函数体内使用 Thread.sleep 代替 delay 来观察主线程在本案例中被阻塞的情况。

Flow 是冷流

Flow 是冷流,与 Sequences 类似——flow 构建器中的代码直到 flow 被收集才会执行:

fun simple(): Flow<Int> = flow { 
    println("Flow started")
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    println("Calling simple function...")
    val flow = simple()
    println("Calling collect...")
    flow.collect { value -> println(value) } 
    println("Calling collect again...")
    flow.collect { value -> println(value) } 
}

打印如下:

Calling simple function...
Calling collect...
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3

从打印可以看出来,第 11 行调用 simple 函数并不会执行 flow 构建器中的代码,因为 simple 函数只是创建并返回了一个 Flow 对象,在调用了 flow.collect 之后才会执行 flow 构建器中的代码。simple 函数本身不需 suspend,实际的挂起操作发生在 collect 调用时,在 flow 构建器的内部。

Flow 的取消原则

Flow 遵循协程的协作取消原则:flow 的收集可以在执行到一个可取消的挂起函数(比如 delay 函数)中挂起的时候取消。以下示例展示了在 withTimeoutOrNull 代码块中运行时,flow 如何在超时的情况下取消并停止执行的:

fun simple(): Flow<Int> = flow { 
    for (i in 1..3) {
        delay(100)          
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    withTimeoutOrNull(250) { // Timeout after 250ms 
        simple().collect { value -> println(value) } 
    }
    println("Done")
}

打印如下:

Emitting 1
1
Emitting 2
2
Done

可以看到上面的 flow 只发射了 2 个数字。

Flow 构建器

前面使用 flow { ... } 构建器创建 flow 是最基础的用法,还有其他创建流的方法。

  • 使用 flowOf 构建器可以创建发射一个固定值的集合的流。
  • 还可以通过调用 Collections 和 Sequences 的 asFlow() 扩展函数将其转成流。

比如,前面的代码中通过 flow 打印 1 到 3 可以改写成下面这样:

fun main(){
    runBlocking {
        // Convert an integer range to a flow
        (1..3).asFlow().collect { value -> println(value) }
    }
}

中间操作符

就像转换集合与序列一样,flow 可以通过操作符来进行转换。上游的 flow 通过操作符可以转换成下游的 flow,这些操作符跟 flow 一样是冷的。

这些操作符与 Sequences 之间的主要区别是这些操作符里面的代码块可以调用挂起函数。

举个例子,即使执行的请求是一个耗时的挂起函数,通过 map 操作符也可以把请求映射到 flow 的结果值上去:

suspend fun performRequest(request: Int): String {
    delay(1000) // imitate long-running asynchronous work
    return "response $request"
}

fun main() = runBlocking<Unit> {
    (1..3).asFlow() // a flow of requests
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

打印如下:

response 1
response 2
response 3

transform 操作符

最通用的流操作符是 transform,它可以用来模仿 map 和 filter 等简单的操作符,也可以实现更复杂的转换。使用 transform 操作符,我们可以任意次数地发射任意个值。

比如,我们可以在执行耗时异步操作前发射一个 String,紧接着打印执行结果:

suspend fun performRequest(request: Int): String {
    delay(1000) // imitate long-running asynchronous work
    return "response $request"
}

suspend fun main(){
    (1..3).asFlow() // a flow of requests
        .transform { request ->
            emit("Making request $request")
            emit(performRequest(request))
        }
        .collect { response -> println(response) }

}

打印如下:

Making request 1
response 1
Making request 2
response 2
Making request 3
response 3

限量操作符

take 是一个限量操作符,它会在到达相应数量的时候取消 flow 的执行,协程中的取消会抛出异常,这样管理资源的函数(比如 try { ... } finally { ... } 代码块)在取消的时候会正常执行。

fun numbers(): Flow<Int> = flow {
    try {                          
        emit(1)
        emit(2) 
        println("This line will not execute")
        emit(3)    
    } finally {
        println("Finally in numbers")
    }
}

fun main() = runBlocking<Unit> {
    numbers() 
        .take(2) // take only the first two
        .collect { value -> println(value) }
}    

这段代码在发射 2 个数字后会停止,打印如下:

1
2
Finally in numbers

末端操作符

Flow 的末端操作符是挂起函数,它会开始 flow 的收集,collect 是最基础的末端操作符,还有其他的末端操作符:

  • 转换成各种集合的,比如 toList 和 toSet。
  • first 操作符用于获取第一个值,single 操作符用于确保 flow 发射单个值。
  • 通过 reduce 和 fold 可以把 flow 减至单个值。

比如下面这段代码只会打印一个值:

val sum = (1..5).asFlow()
    .map { it * it } // squares of numbers from 1 to 5                           
    .reduce { a, b -> a + b } // sum them (terminal operator)
println(sum)

打印如下:

55

Flow 是有序的

对于一个单一的、独立的 flow,其中元素的处理是“顺序的”或“串行的”,除非使用了那些能操作多个流的特殊操作符(比如合并型操作符 Merge、缓冲型操作符 Buffer 等等),流的收集操作直接发生在调用末端操作符(如 collect、first()、single()、toList()等)的那个协程中,默认情况下不会启动新的协程。每个发射的值都会按从上游到下游的顺序经过所有中间操作符的处理,并最终传给末端操作符。

下面的示例会过滤偶数并将其映射成字符串:

(1..5).asFlow()
    .filter {
        println("Filter $it")
        it % 2 == 0              
    }              
    .map { 
        println("Map $it")
        "string $it"
    }.collect { 
        println("Collect $it")
    }  

打印如下:

Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5

Flow context

Flow 的收集操作总是在调用末端操作符的协程的上下文中执行,无论 Flow 本身是如何实现的,比如下面这段代码传入了 context,打印的代码就是在 context 中执行的,无论 simple() 这个 Flow 在内部如何发射值,flow 的这个属性称为上下文保持。

withContext(context) {
    simple().collect { value ->
        println(value) // run in the specified context
    }
}

因此,默认情况下,flow { ... } 构建器中的代码是在该 Flow 的收集器(Collector)所提供的上下文中运行的。比如下面这段代码:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun simple(): Flow<Int> = flow {
    log("Started simple flow")
    for (i in 1..3) {
        emit(i)
    }
}  

fun main() = runBlocking<Unit> {
    simple().collect { value -> log("Collected $value") } 
}  

打印如下:

[main @coroutine#1] Started simple flow 
[main @coroutine#1] Collected 1 
[main @coroutine#1] Collected 2 
[main @coroutine#1] Collected 3

由于 simple().collect 是在主线程调用的,所以 simple 函数中的 flow 代码块也是在主线程中被调用的。这对于快速运行或异步的代码来说是完美的默认设置,这些代码不关心执行上下文,也不会阻塞调用者。

使用 withContext 的常见陷阱

然而,耗时耗 CPU 的代码可能需要在 Dispatchers.Default 的 context 中执行,更新 UI 的代码可能需要在 Dispatchers.Main 的 context 中执行。withContext 一般用于在协程中切换 context,但是 flow { ... } 中的代码会保留调用流的收集函数的 context,不允许在另一个 context 中调用 emit 函数。看下面的代码:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simple(): Flow<Int> = flow {
    // The WRONG way to change context for CPU-consuming code in flow builder
    kotlinx.coroutines.withContext(Dispatchers.Default) {
        for (i in 1..3) {
            Thread.sleep(100) // pretend we are computing it in CPU-consuming way
            emit(i) // emit next value
        }
    }
}

fun main() = runBlocking<Unit> {
    simple().collect { value -> println(value) }
}

这段代码会报异常:

Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
		Flow was collected in [BlockingCoroutine{Active}@404fa54d, BlockingEventLoop@4d65d10b],
		but emission happened in [DispatchedCoroutine{Active}@d7be2e4, Dispatchers.Default].
		Please refer to 'flow' documentation or use 'flowOn' instead
	at ...

flowOn 操作符

上面的报错信息指出你需要使用 flowOn 函数来改变 flow 发射的 context,正确的改变 flow 的 context 的方式如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        Thread.sleep(100) // pretend we are computing it in CPU-consuming way
        log("Emitting $i")
        emit(i) // emit next value
    }
}.flowOn(Dispatchers.Default) // RIGHT way to change context for CPU-consuming code in flow builder

fun main() = runBlocking<Unit> {
    simple().collect { value ->
        log("Collected $value") 
    } 
}  

运行后打印如下:

[DefaultDispatcher-worker-1] Emitting 1
[main] Collected 1
[DefaultDispatcher-worker-1] Emitting 2
[main] Collected 2
[DefaultDispatcher-worker-1] Emitting 3
[main] Collected 3

从打印可以看出 flow { ... } 代码块中的代码运行在后台线程中,而 collect { ... } 代码块中的代码运行在主线程中。

另外注意到 flowOn 操作符改变了 flow 默认顺序执行的属性,现在收集运行在一个协程中,而发射运行在另一个线程的另一个协程中,并且这个协程与收集运行的协程是并行的,这里通过 flowOn 操作符创建了另一个协程。

缓冲

让 Flow 的不同部分在不同的协程中运行,有助于减少整个 Flow 收集过程的总耗时,特别是当涉及长时间运行的异步操作时。比如下面这段代码,flow 的发射耗时 100 ms,flow 的收集耗时 300 ms,那么这个 flow 总共耗时多少呢:

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // pretend we are asynchronously waiting 100 ms
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> { 
    val time = measureTimeMillis {
        simple().collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
    }   
    println("Collected in $time ms")
}

打印如下:

1 
2 
3 
Collected in 1220 ms

从打印可以看到,总共耗时 1200 ms 以上(3 个数字,每个花费 400 ms)。

我们可以使用 buffer 操作符让发射的代码并发执行,代码如下:

val time = measureTimeMillis {
    simple()
        .buffer() // buffer emissions, don't wait
        .collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
}   
println("Collected in $time ms")

打印如下:

1 
2 
3 
Collected in 1071 ms

这段代码耗时更少,因为我们有效地创建了一个处理管道,只需要在发射的时候等待 100 ms 以及处理每个数字各需花费的 300 毫秒,这样总耗时大概 1000 ms。

注意,flowOn 操作符在需要更改 CoroutineDispatcher 时使用了相同的缓冲机制,但 flowOn 操作符改变了执行上下文,buffer 操作符明确请求缓冲而不改变执行上下文。

合并

有一种情况,当一个 Flow 代表 操作的部分结果 或 操作状态更新 时,你可能不需要处理每一个值,只需要处理最新的值,当收集器处理速度太慢时,你可以使用 conflate 操作符来跳过中间值,用法如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // pretend we are asynchronously waiting 100 ms
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> { 
    val time = measureTimeMillis {
        simple()
            .conflate() // conflate emissions, don't process each one
            .collect { value -> 
                delay(300) // pretend we are processing it for 300 ms
                println(value) 
            } 
    }   
    println("Collected in $time ms")
}

打印如下:

1 
3 
Collected in 758 ms

因为正在处理第 1 个数字的时候,第 2 和第 3 个数字已经产生了,conflate 的策略是:保留最新的,丢弃中间的。所以跳过了第 2 个数字,只有最新的(第3个数字)被传递给了收集器。

处理最新的值

当发射器(emmitter)和收集器(collector)都很慢的时候,使用 conflate 操作符是其中一种方式来加快速度,它是通过丢弃发射的值来实现的。另一种方式是取消慢速的收集器,并在每次有新值发射时重新启动它。有一系列名字叫 xxxLastest 的操作符,它们执行与对应的 xxx 操作符相同的基本逻辑,但是会在新值到达时,取消执行 xxxLastest{...} 代码块中的代码。代码如下:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // pretend we are asynchronously waiting 100 ms
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> { 
    val time = measureTimeMillis {
        simple()
            .collectLatest { value -> // cancel & restart on the latest value
                println("Collecting $value") 
                delay(300) // pretend we are processing it for 300 ms
                println("Done $value") 
            } 
    }   
    println("Collected in $time ms")
}

打印如下:

Collecting 1
Collecting 2
Collecting 3
Done 3
Collected in 741 ms

由于 collectLatest { ... } 代码块中的代码执行完需要耗时 300 ms 以上,但是新的数值每隔 100 ms 发射一次,我们可以看到 Collecting 会执行 3 次,但是 Done 只会在最后一次执行。

组合多个 flow

有很多组合多个 flow 的方法。

Zip

类似 Kotlin 标准库中的 Sequence.zip 扩展函数,flow 也有 zip 操作符,它可以结合两个 flow 中对应的数值:

val nums = (1..3).asFlow() // numbers 1..3
val strs = flowOf("one", "two", "three") // strings 
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string
    .collect { println(it) } // collect and print

打印如下:

1 -> one
2 -> two
3 -> three

Combine

当 flow 表示一个变量或操作的最新值时(请参阅前面的 conflate 操作符),可能需要对相应流的最新值进行计算,并且每当上游流发射值的时候都需要重新计算,相应的一组操作符称为 combine。

例如,如果前面示例中的数字每 300 ms 更新一次,但字符串每 400 ms 更新一次, 使用 zip 操作符合并它们仍会产生相同的结果, 尽管结果变成了每 400 ms 打印一次:

在本示例中使用了 onEach 操作符让代码更直观和简洁。

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time 
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string with "zip"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

如果这里使用 combine 操作符会得到不同的输出结果:

val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms          
val startTime = System.currentTimeMillis() // remember the start time 
nums.combine(strs) { a, b -> "$a -> $b" } // compose a single string with "combine"
    .collect { value -> // collect and print 
        println("$value at ${System.currentTimeMillis() - startTime} ms from start") 
    } 

打印结果如下:

1 -> one at 444 ms from start 
2 -> one at 647 ms from start 
2 -> two at 846 ms from start 
3 -> two at 947 ms from start 
3 -> three at 1246 ms from start

这里 nums 或 strs 流中的每次发射数据都会打印一行。

展平流

Flows 表示的是异步接收的值序列,因此很容易遇到这样的情况:每个值都需要触发请求另一个值的序列。比如,有这样一个 flow 会发射两个字符串,发射间隔为 500 ms:

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // 等待 500 毫秒
    emit("$i: Second")    
}

现在如果我们有一个包含三个整数的流,并对每个整数调用 requestFlow,如下所示:

(1..3).asFlow().map { requestFlow(it) }

然后我们会得到一个包含流的流(Flow<Flow<String>>),需要将其展平为单个流以进行下一步处理。集合与序列都拥有 flatten 与 flatMap 操作符来做这件事。然而,由于流具有异步的特性,因此需要不同的展平模式,因此,存在一组流的展平操作符。

flatMapConcat

通过 flatMapConcat 与 flattenConcat 操作符可以将流的流串联起来,它们等待内部流执行完之后开始收集下一个值,如下面的示例所示:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // wait 500 ms
    emit("$i: Second")    
}

fun main() = runBlocking<Unit> { 
    val startTime = currentTimeMillis() // remember the start time 
    (1..3).asFlow().onEach { delay(100) } // emit a number every 100 ms 
        .flatMapConcat { requestFlow(it) }                                                                           
        .collect { value -> // collect and print 
            println("$value at ${currentTimeMillis() - startTime} ms from start") 
        } 
} 

打印如下:

1: First at 267 ms from start
1: Second at 782 ms from start
2: First at 894 ms from start
2: Second at 1405 ms from start
3: First at 1515 ms from start
3: Second at 2032 ms from start

flatMapMerge

另一种展平操作是并发收集所有传入的 flow,并将它们的值合并成一个单独的 flow,以便尽快的发射结果值。 通过 flatMapMerge 与 flattenMerge 操作符可以实现这一目的,它们都接收一个concurrency 参数, 该参数用于限制同时收集的并发的流的个数 (默认为 DEFAULT_CONCURRENCY)。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // wait 500 ms
    emit("$i: Second")    
}

fun main() = runBlocking<Unit> { 
    val startTime = currentTimeMillis() // remember the start time 
    (1..3).asFlow().onEach { delay(100) } // a number every 100 ms 
        .flatMapMerge { requestFlow(it) }                                                                           
        .collect { value -> // collect and print 
            println("$value at ${currentTimeMillis() - startTime} ms from start") 
        } 
}

打印如下:

1: First at 378 ms from start
2: First at 465 ms from start
3: First at 578 ms from start
1: Second at 895 ms from start
2: Second at 972 ms from start
3: Second at 1090 ms from start

从打印可以看到使用 flatMapMerge 操作符的并发特性很明显。

注意,flatMapMerge 会顺序调用代码块(本示例中的 { requestFlow(it) }),但是会并发执行流的收集,相当于执行顺序是先执行 map { requestFlow(it) } 然后在其返回结果上调用 flattenMerge。

flatMapLatest

与 collectLatest 操作符类似,也有相对应的“最新”的展平模式,在发射新流后会立即取消之前的流的收集,该操作由 flatMapLatest 操作符来实现。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun requestFlow(i: Int): Flow<String> = flow {
    emit("$i: First") 
    delay(500) // wait 500 ms
    emit("$i: Second")    
}

fun main() = runBlocking<Unit> { 
    val startTime = currentTimeMillis() // remember the start time 
    (1..3).asFlow().onEach { delay(100) } // a number every 100 ms 
        .flatMapLatest { requestFlow(it) }                                                                           
        .collect { value -> // collect and print 
            println("$value at ${currentTimeMillis() - startTime} ms from start") 
        } 
}

如下打印很好地展示了 flatMapLatest 是如何工作的:

1: First at 417 ms from start
2: First at 539 ms from start
3: First at 653 ms from start
3: Second at 1167 ms from start

注意到 flatMapLatest 在收到一个新值时取消了代码块中的所有代码 (本示例中的 { requestFlow(it) })的执行。 这在该示例中也是一样的,由于调用 requestFlow 的速度是很快的,不会发生挂起的情况, 所以刚开始不会取消。但是在 requestFlow 函数中调用了 delay 这种挂起函数就不一样了,输出就会变得不一样。

异常

当发射器或操作符中的代码抛出异常的时候,flow 可以执行到捕获异常结束。有多种处理异常的方法。

对收集器 try catch

对收集器可以使用 Kotlin 的 try catch 代码块来处理异常:

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value ->         
            println(value)
            check(value <= 1) { "Collected $value" }
        }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}  

这段代码在终止操作符 collect 中成功地捕获了异常,可以看到,捕获异常后就不会有新值发射出来了。

Emitting 1 
1 
Emitting 2 
2 
Caught java.lang.IllegalStateException: Collected 2

上面这种方式不止可以捕获过渡操作符或终止操作符中抛出的异常, 还可以捕获发射器中抛出的异常。把上面的代码改一下,把发射的代码映射成字符串,并在映射的代码中生成一个异常:

fun simple(): Flow<String> = 
    flow {
        for (i in 1..3) {
            println("Emitting $i")
            emit(i) // emit next value
        }
    }
    .map { value ->
        check(value <= 1) { "Crashed on $value" }                 
        "string $value"
    }

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value -> println(value) }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}            

打印如下:

Emitting 1 
string 1 
Emitting 2 
Caught java.lang.IllegalStateException: Crashed on 2

可以看到异常还是会被捕获然后终止收集。

异常的透明性

流必须对异常透明,在 flow { ... } 构建器内部的 try/catch 代码块中发射值是违反异常的透明性的,那么发射器的代码该如何封装其异常处理呢?

发射器可以使用 catch 操作符来保留此异常的透明性并封装其异常处理,catch 操作符的代码块可以分析异常并根据捕获到的异常以不同的方式进行处理:

  • 可以使用 throw 重新抛出异常。
  • 可以使用 catch 代码块中的 emit 将异常转换为值发射出去。
  • 可以将异常忽略,或用日志打印,或使用其他的代码处理它。

例如,我们可以在捕获到异常的时候发射文本:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*


fun simple(): Flow<String> =
    flow {
        for (i in 1..3) {
            println("Emitting $i")
            emit(i) // emit next value
        }
    }
        .map { value ->
            check(value <= 1) { "Crashed on $value" }
            "string $value"
        }

fun main() = runBlocking<Unit> {
    simple()
        .catch { e -> emit("Caught $e") } // emit on exception
        .collect { value -> println(value) }
}

打印如下:

Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: Crashed on 2

输出跟上面一样,只不过这样就不需要写 try/catch 语句了。

透明捕获

catch 操作符遵循异常透明性,仅捕获上游异常(即来自 catch 语句上面的操作符产生的异常)。 如果 collect { ... } 块(位于 catch 之下)抛出异常,那么异常会逃逸:

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .catch { e -> println("Caught $e") } // does not catch downstream exceptions
        .collect { value ->
            check(value <= 1) { "Collected $value" }
            println(value)
        }
}

打印如下:

Emitting 1
1
Emitting 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
	at ...

可以看到异常没有被上面的 catch 语句捕获到。

声明式捕获

我们可以将 catch 操作符的声明性与处理所有异常的期望相结合,将前面 collect 操作符中的代码块移到 onEach 中,并将其放到 catch 操作符之前。这样,收集流可以调用无参的 collect() 来触发:

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .onEach { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
        .catch { e -> println("Caught $e") }
        .collect()
}  

打印如下:

Emitting 1
1
Emitting 2
Caught java.lang.IllegalStateException: Collected 2

现在可以看到已经打印了“Caught ...”的消息,并且这样就不用显式地使用 try/catch 语句了。

完成

当流执行完毕时(不管是普通完成还是异常完成)可能需要执行一个动作, 可以通过两种方式实现:命令式或声明式。

命令式 finally 块

除了 try/catch 之外,收集器还能使用 finally 语句在 collect 完成时执行一个动作。

fun simple(): Flow<Int> = (1..3).asFlow()

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value -> println(value) }
    } finally {
        println("Done")
    }
} 

打印如下:

1 
2 
3 
Done

上面的代码打印完数字之后打印了一个“Done”字符串。

声明式处理

对于声明式的方法,flow 有 onCompletion 过渡操作符,它在流完成收集时调用。

前面的代码改成使用 onCompletion 操作符,代码如下:

simple()
    .onCompletion { println("Done") }
    .collect { value -> println(value) }

输出跟上面是一样的。

onCompletion 操作符的优势是它带一个可空的 Throwable 类型的参数,这个参数可以用来确定 flow 的收集是正常结束还是异常结束的。下面示例中的 flow 会在发射数字 1 后抛出异常:

fun simple(): Flow<Int> = flow {
    emit(1)
    throw RuntimeException()
}

fun main() = runBlocking<Unit> {
    simple()
        .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
        .catch { cause -> println("Caught exception") }
        .collect { value -> println(value) }
}  

打印如下:

1 
Flow completed exceptionally 
Caught exception

onCompletion 操作符与 catch 不同,它不处理异常,从上面的例子可以看出来,异常还是会流向下游。

成功完成

与 catch 操作符的另一个不同点是 onCompletion 能看到所有的异常并且仅在上游 flow 成功完成(没有取消或失败)的情况下接收一个 null 异常。

fun simple(): Flow<Int> = (1..3).asFlow()

fun main() = runBlocking<Unit> {
    simple()
        .onCompletion { cause -> println("Flow completed with $cause") }
        .collect { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
}

由于下游 flow 抛出了异常,所以 onCompletion 中的 cause 不为空,打印如下:

1
Flow completed with java.lang.IllegalStateException: Collected 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
	at ...

命令式还是声明式

现在我们知道了如何收集 flow,并以命令式与声明式的方式处理其完成及异常,问题是应该首选哪种方式呢。作为一个库,我们不主张采用任何特定的方式,并且相信这两种选择都是有效的, 应该根据自己的喜好与代码风格进行选择。

启动流

使用流表示来自一些数据源的异步事件是很简单的。 在这个案例中,我们需要一个类似 addEventListener 的函数,该函数注册一段响应的代码处理即将到来的事件,并继续进行进一步的处理。onEach 操作符可以担任该角色。 然而,onEach是一个过渡操作符。我们也需要一个终止操作符来收集流,否则仅调用 onEach 是无效的。

如果我们在 onEach 之后使用 collect 终止操作符,那么后面的代码会一直等待直至 flow 被收集:

// Imitate a flow of events
fun events(): Flow<Int> = (1..3).asFlow().onEach { delay(100) }

fun main() = runBlocking<Unit> {
    events()
        .onEach { event -> println("Event: $event") }
        .collect() // <--- Collecting the flow waits
    println("Done")
}   

打印如下:

Event: 1
Event: 2
Event: 3
Done

launchIn 终止操作符在这里可以派上用场。使用 launchIn 替换 collect 我们可以在单独的协程中启动流的收集:

fun main() = runBlocking<Unit> {
    events()
        .onEach { event -> println("Event: $event") }
        .launchIn(this) // <--- Launching the flow in a separate coroutine
    println("Done")
}  

打印如下:

Done 
Event: 1 
Event: 2 
Event: 3

launchIn 必须传一个参数 CoroutineScope,用于指定用哪个协程来启动 flow 的收集。前面示例中的作用域来自 runBlocking 协程构建器,在这个 flow 运行的时候,runBlocking 作用域会等待它的子协程执行完毕并阻止 main 函数返回终止此示例。

在实际的 App 中,作用域来自于一个寿命有限的实体。在该实体终止后,相应的作用域就会被取消,即取消相应 flow 的收集。这种成对的 onEach {... } .launchIn(scope) 工作方式就像 addEventListener 一样。但是这种方式不需要调用相应的 removeEventListener 函数, 因为取消与结构化并发已经帮我们实现了。

注意,launchIn 也会返回一个 Job ,可以在不取消整个作用域的情况下仅取消相应的 flow 的收集,或者可以对其执行 join 操作。

流取消检测

为方便起见,flow 构建器会对每个发射值执行附加的 ensureActive 检查,检查该值是否取消发射,这意味着通过 flow { ... } 代码块进行循环发射是可以取消的:

fun foo(): Flow<Int> = flow { 
    for (i in 1..5) {
        println("Emitting $i") 
        emit(i) 
    }
}

fun main() = runBlocking<Unit> {
    foo().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

打印如下:

Emitting 1
1
Emitting 2
2
Emitting 3
3
Emitting 4
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@78479f2b

可以看到,尝试发射数字 4 的时候程序报异常了。

但是,考虑到性能问题,大多数其他 flow 操作符不会自行执行附加的取消检测。例如,如果你使用 IntRange.asFlow 扩展函数来编写相同的循环逻辑,并且如果里面没有添加挂起函数,那么就没有取消的检测。

fun main() = runBlocking<Unit> {
    (1..5).asFlow().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

打印如下:

1
2
3
4
5
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job=BlockingCoroutine{Cancelled}@25f0c5e7

所有的数字都打印了,并且仅在 runBlocking 返回之前才检测到了取消。

让繁忙的 flow 可以取消

在协程处于繁忙循环执行的情况下,你必须显式地标明是否可以取消,你可以添加 .onEach { currentCoroutineContext().ensureActive() }, 但是有一个现成的 cancellable 操作符来执行此操作:

fun main() = runBlocking<Unit> {
    (1..5).asFlow().cancellable().collect { value -> 
        if (value == 3) cancel()  
        println(value)
    } 
}

打印如下:

1 
2 
3 
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled; job="coroutine#1":BlockingCoroutine{Cancelled}@5ec0a365

这样收集到数字 3 的时候 flow 就取消了,并抛出异常。

流(Flow)与响应式流(Reactive Streams)

对于熟悉响应式流(Reactive Streams)或诸如 RxJava 与 Project Reactor 这样的响应式框架的人来说,flow 的设计也许看起来会非常熟悉。

确实,其设计灵感来源于响应式流及其各种实现,但是 flow 的主要目标是拥有尽可能简单的设计, 对 Kotlin 及挂起友好,且遵从结构化并发。你可以通过阅读 Reactive Streams and Kotlin Flows 这篇文章来了解 flow 的更多故事。

虽然有所不同,但从概念上讲,flow 依然是响应式流,并且可以将它转换为响应式 Publisher,反之亦然。 这些开箱即用的转换器由kotlinx.coroutines 提供, 可以在的相应的响应式模块(kotlinx-coroutines-reactive 用于 Reactive Streams,kotlinx-coroutines-reactor 用于 Project Reactor,kotlinx-coroutines-rx2/kotlinx-coroutines-rx3 用于 RxJava2/RxJava3)中找到。 集成模块包含了 flow 与其他实现之间的转换,与 Reactor 的 Context 的集成以及与一系列响应式实体一起使用的挂起友好的使用方式。