[译] Kotlin Flow 官方 Guide 指南(2021-06-13)翻译

本文详细介绍了Kotlin协程的Flow特性,包括Flow的冷流性质、取消基础、构建器、中间运算符、变换运算符、大小限制运算符、终端运算符、顺序性、上下文、错误处理、流的合并与展平、异常处理以及流的启动和取消检查。通过实例展示了Flow在异步编程中的各种操作和优化技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接:https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/topics/flow.md

Flow 是冷流 Flows are cold

flow 构建器中的代码在收集流之前不会运行。

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

fun main() = runBlocking {
    println("Calling simple function...")
    val flow = simple() // simple() 调用快速返回并且不等待任何东西
    println("Calling collect...")
    flow.collect { value -> println(value) }
    println("Calling collect again...")
    flow.collect { value -> println(value) }
}
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3

Flow 取消基础知识 Flow cancellation basics

Flow 遵循协程的一般协作取消。像通常一样,当流在可取消的挂起函数(如 delay)中被挂起时,可以取消流收集。

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

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

Flow 构建器 Flow builders

flow { … } 构建器是最基本的构建器。还有其他构建器可以更轻松地声明流:

  • flowOf 构建器,用于定义发出一组固定值的流。
  • 可以使用 .asFlow() 扩展函数将各种集合和序列转换为流。
fun main() = runBlocking {
    // Convert an integer range to a flow
    (1..3).asFlow().collect { value -> println(value) }
}

中间运算符 Intermediate flow operators

流可以使用运算符进行转换。中间运算符应用于上游流并返回下游流。这些运算符是冷的,就像流一样。对此类运算符的调用本身不是挂起函数,它执行的很快,返回一个新的转换流的定义。但这些运算符中的代码块可以调用挂起函数。

例如,传入请求流可以使用 map 运算符映射到结果,即使执行请求是由挂起函数实现的长时间运行的操作:

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

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

变换运算符 Transform operator

在流变换算子中,最通用的一种叫做变换。它可以用来模仿简单的转换,比如 map 和 filter,也可以实现更复杂的转换。使用变换运算符,我们可以发出任意次数的任意值。

例如,使用转换,我们可以在执行长时间运行的异步请求之前发出一个字符串,并在其后响应:

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

fun main() = runBlocking {
    (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

大小限制运算符 Size-limiting operators

当达到相应的限制时,像 take 这样的大小限制中间运算符会取消流的执行。协程中的取消总是通过抛出异常来执行,以便所有资源管理功能(如 try { … } finally { … } 块)在取消的情况下正常运行。

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

fun main() = runBlocking {
    numbers()
        .take(2) // take only the first two
        .collect { value -> println(value) }
}
1
2
Caught kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed
Finally in numbers

终端运算符 Terminal flow operators

流上的终端运算符是启动流收集的挂起函数。collect 运算符是最基本的终端运算符,但还有其他终端运算符,它们可以更容易:

  • 转换为各种集合,如 toList 和 toSet。
  • 获取第一个值并确保流发出单个值的运算符。
  • 使用 reduce 和 fold 将流减少到一个值。
fun main() = runBlocking {
    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

流是顺序的 Flows are sequential

除非使用对多个流进行操作的特殊运算符,否则流的每个单独收集都按顺序执行。该收集直接在调用终端运算符的协程中工作。默认情况下不会启动新的协程。每个发出的值都由从上游到下游的所有中间运算符处理,然后传递给终端运算符。

请参阅以下示例,该示例过滤偶数整数并将它们映射到字符串:

fun main() = runBlocking {
    (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

流的收集总是发生在调用协程的上下文中。

例如,如果有一个 simple 流,那么下面的代码运行在这段代码的作者指定的上下文中,而不管 simple 流的实现细节:

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

流的这种属性称为上下文保留
因此,默认情况下,flow { … } 构建器中的代码在相应流的收集器提供的上下文中运行。

例如,考虑一个 simple 函数的实现,该函数打印调用它的线程并发出三个数字:

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 {
    simple().collect { value -> log("Collected $value") }
}
[main] Started simple flow
[main] Collected 1
[main] Collected 2
[main] Collected 3

由于 simple().collect 是从主线程调用的,simple 流的主体也在主线程中被调用。对于不关心执行上下文且不阻塞调用者的快速运行的或异步执行的代码,这是完美的默认设置。

withContext 错误的发射 Wrong emission withContext

通常, 在使用 Kotlin 协程的代码中用 withContext 来更改上下文,但是 flow { … } 构建器中的代码必须遵守上下文保留属性,并且不允许从不同的上下文发出值。

fun simple(): Flow<Int> = flow {
    // The WRONG way to change context for CPU-consuming code in flow builder
    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 {
    simple().collect { value -> println(value) }
}
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
		Flow was collected in [BlockingCoroutine{Active}@298e2c81, BlockingEventLoop@16e7d2b7],
		but emission happened in [DispatchedCoroutine{Active}@54f29d84, Dispatchers.Default].
		Please refer to 'flow' documentation or use 'flowOn' instead

flowOn 运算符 flowOn operator

上面的异常指明应该使用 flowOn 函数来更改流发射的上下文
下面的示例展示了更改流上下文的正确方法,该示例还打印了相应线程的名称以表明其工作原理:

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 {
    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 { … } 在后台线程中是如何工作的,而收集发生在主线程中。

转载请说明出处:https://blog.csdn.net/hegan2010/article/details/117880934

缓冲区 Buffering

从收集流所需的总时间的角度来看,在不同协程中运行流的不同部分可能会有所帮助,尤其是在涉及长时间运行的异步操作时。
例如,考虑一个 simple 流的发射速度很慢的情况,需要 100ms 才能产生一个元素; 并且收集器也很慢,需要 300ms 来处理一个元素。我们来看看收集这样一个三个数字的流需要多长时间:

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

fun main() = runBlocking {
    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 1554 ms (200 + 300 + 200 + 300 + 200 + 300)

我们可以在流上使用 buffer 运算符来同时运行 simple 流的发射代码和收集代码,而不是顺序运行它们:

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

fun main() = runBlocking {
    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 1308 ms (200 + 300 + 300 + 300)

它生成和以上相同的数字的速度更快,因为我们有效地创建了一个处理管道,只需等待 200ms 即可获得第一个数字,然后仅花费 300ms 来处理每个数字。这样运行大约需要 1100ms。

合流 Conflation

当流表示操作的部分结果或操作状态更新时,可能不需要处理每个值,而是只处理最近的值。在这种情况下,当收集器太慢而无法处理中间值时,可以使用 conflate 运算符跳过中间值。

基于上一个示例:

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 {
    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 1204 ms

处理最后一个值 Processing the latest value

当发射和收集都很慢时,合并是一种加速处理的方法。它通过删除发出的值来实现。另一种方法是取消慢速收集器并在每次发出新值时重新启动它。有一系列的 xxxLatest 运算符执行与 xxx 运算符相同的基本逻辑,但有新值时取消执行其块中的代码。

让我们尝试将前面示例中的 conflate 更改为 collectLatest:

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 {
    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 793 ms

组合多个流 Composing multiple flows

zip

组合两个流的对应值。

fun main() = runBlocking {
    val nums = (1..4).asFlow() // numbers 1..4
    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

当流表示变量或操作的最新值时,可能需要执行依赖于相应流的最新值的计算,并在任何上游流发出值时重新计算它。
每次都使用相应流的最新值产生一个组合。

fun main() = runBlocking {
    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 640 ms from start
2 -> one at 839 ms from start
2 -> two at 1043 ms from start
3 -> two at 1144 ms from start
3 -> three at 1447 ms from start

如果用Zip:

fun main() = runBlocking {
    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")
        }
}
1 -> one at 466 ms from start
2 -> two at 867 ms from start
3 -> three at 1275 ms from start

展平流 Flattening flows

流表示异步接收的值序列,因此很容易陷入每个值又触发请求另一个值序列的情况。
例如,我们可以使用以下函数返回相距 500ms 的两个字符串流:

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

现在,如果我们有一个由三个整数组成的流,并像这样为每个整数调用 requestFlow:

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

它返回的流的类型是

Flow<Flow<String>>

需要将其展平为单个流以进行进一步处理。

flatMapConcat / flattenConcat

在开始收集下一个之前等待内部流完成。

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

fun main() = runBlocking {
    val startTime = System.currentTimeMillis() // remember the start time
    (1..3).asFlow().onEach { delay(100) } // a number every 100 ms
        .flatMapConcat {
            requestFlow(it)
        }
        .collect { value -> // collect and print
            println("$value at ${System.currentTimeMillis() - startTime} ms from start")
        }
}
1: First at 166 ms from start
1: Second at 670 ms from start
2: First at 772 ms from start
2: Second at 1272 ms from start
3: First at 1376 ms from start
3: Second at 1879 ms from start

flatMapMerge / flattenMerge

并发收集所有传入的流并将它们的值合并到一个流中,以便尽快发出值。
接受一个可选的 concurrency 参数,该参数限制同时收集的并发流的数量(默认情况下等于 DEFAULT_CONCURRENCY)。

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

fun main() = runBlocking {
    val startTime = System.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 ${System.currentTimeMillis() - startTime} ms from start")
        }
}
1: First at 318 ms from start
2: First at 410 ms from start
3: First at 515 ms from start
1: Second at 822 ms from start
2: Second at 914 ms from start
3: Second at 1022 ms from start

注意:flatMapMerge 按顺序调用其代码块(在此示例中为 { requestFlow(it) }),但并发收集结果流,这相当于先执行顺序map { requestFlow(it) }, 然后在结果上调用 flattenMerge。
也就是说:

        .flatMapMerge {
            requestFlow(it)
        }

等价于

        .map { it: Int ->
            requestFlow(it)
        }
        .flatMapMerge { it: Flow<String> ->
            it
        }

flatMapLatest

与 collectLatest 运算符类似,一旦发出新流,就取消先前流的收集,并存在相应的“最新”展平模式。

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

fun main() = runBlocking {
    val startTime = System.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 ${System.currentTimeMillis() - startTime} ms from start")
        }
}
1: First at 326 ms from start
2: First at 432 ms from start
3: First at 536 ms from start
3: Second at 1039 ms from start

请注意, flatMapLatest 在新值上取消其块(在此示例中为 { requestFlow(it) })中的所有代码。 在这个特定的例子中没有区别,因为对 requestFlow 的调用本身很快,没有挂起,并且不能被取消。 但是,如果我们在那里使用诸如 delay 之类的挂起功能,它就会显露出来。

流异常 Flow exceptions

当运算符内的发射器或代码抛出异常时,流收集可以以异常结束。有几种方法可以处理这些异常。

收集器 try 和 catch 块 Collector try and 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 {
    try {
        simple().collect { value ->
            println(value)
            check(value <= 1) { "Collected $value" }
        }
    } catch (e: Throwable) {
        println("Caught $e")
    }
}
Emitting 1
1
Emitting 2
2
Caught java.lang.IllegalStateException: Collected 2

一切都可以捕获 Everything is caught

前面的示例实际上捕获了在发射器或任何中间或终端运算符中发生的任何异常。

例如,让我们更改代码,以便将发出的值映射到字符串,但相应的代码会产生异常:

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 {
    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

异常透明性 Exception transparency

但是发射器的代码如何封装它的异常处理行为呢?

流必须对异常透明,并且在 flow { … } 构建器中从 try/catch 块内部发出值违反了异常透明性。 这保证了抛出异常的收集器总是可以像前面的例子一样使用 try/catch 来捕获它。

发射器可以使用 catch 运算符来保持这种异常透明性并允许对其异常处理进行封装。catch 运算符的主体可以分析异常并根据捕获的异常以不同的方式对其做出反应:

  • 可以用 throw 重新抛出异常。
  • 可以在 catch 的主体中的使用 emit 将异常转换为值的发射。
  • 异常可以被忽略、记录或其他代码处理。

例如,让我们在捕获异常时发出字符串:

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 {
    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

透明捕获 Transparent catch

catch 中间运算符,尊重异常透明性,只捕获上游异常(即来自 catch 之上的所有运算符的异常,但不在其之下)。如果 collect { … } 中的块(位于 catch 下方)抛出异常,则它会逃逸。

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

fun main() = runBlocking {
    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

声明式捕获 Catching declaratively

我们可以将 catch 运算符的声明性质与处理所有异常的愿望结合起来,通过将 collect 运算符的主体移动到 onEach 并将其放在 catch 运算符之前。此流的收集必须由不带参数的 collect() 调用触发。

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

fun main() = runBlocking {
    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

流完成 Flow completion

当流收集完成(正常或异常)时,它可能需要执行一个操作。您可能已经注意到,它可以通过两种方式完成:命令式声明式

命令式 finally 块 Imperative finally block

除了 try/catch 之外,收集器还可以使用 finally 块在收集完成后执行操作。

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

fun main() = runBlocking {
    try {
        simple().collect { value -> println(value) }
    } finally {
        println("Done")
    }
}
1
2
3
Done

声明式处理 Declarative handling

对于声明式方法,流具有 onCompletion 中间运算符,当流完成收集时调用该运算符。
可以使用 onCompletion 运算符重写前面的示例并产生相同的输出:

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

fun main() = runBlocking {
    simple()
        .onCompletion { println("Done") }
        .collect { value -> println(value) }
}
1
2
3
Done

onCompletion 的主要优点是 lambda 的一个可为空的 Throwable 参数,可用于确定流收集是正常完成还是异常完成。

在以下示例中,simple 流在发出数字 1 后抛出异常:

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

fun main() = runBlocking {
    simple()
        .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
        .catch { cause -> println("Caught exception: $cause") }
        .collect { value -> println(value) }
}
1
Flow completed exceptionally
Caught exception: java.lang.RuntimeException

与 catch 不同,onCompletion 运算符不处理异常。从上面的示例代码我们可以看到,异常仍然流向下游。它将被传递给更多的 onCompletion 运算符,并且可以使用 catch 运算符进行处理。

成功完成 Successful completion

与 catch 运算符的另一个区别是 onCompletion 会看到所有异常,并且仅在成功完成上游流(没有取消或失败)时才会收到空异常。

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

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

我们可以看到完成 cause 不为空,因为下游异常导致流中止:

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

启动流 Launching flow

使用流来表示来自某个源的异步事件很容易。在这种情况下,我们需要一个类似 addEventListener 的函数来注册一段代码,对传入的事件做出反应并继续进一步的工作。onEach 运算符可以充当这个角色。然而,onEach 是一个中间运算符。我们还需要一个终端运算符来收集流。否则,仅调用 onEach 无效。
如果我们在 onEach 之后使用 collect 终端运算符,那么它后面的代码会一直等到流被收集。

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

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

launchIn 终端运算符在这里派上用场。通过将 collect 替换为 launchIn,我们可以在单独的协程中启动流的收集,以便立即继续执行其他代码

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

fun main() = runBlocking {
    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,在其中启动收集流的协程。
在上面的例子中,这个 scope 来自 runBlocking 协程构建器,所以当流运行时,这个 runBlocking scope 等待它的子协程完成,并防止主函数返回和终止这个例子。

在实际应用中,scope 将来自具有有限生命周期的实体。一旦该实体的生命周期终止,相应的 scope 就会被取消,从而取消相应流的收集。这样,一对 onEach { … }.launchIn(scope) 就像 addEventListener 一样工作。但是,不需要相应的 removeEventListener 函数,因为取消和结构化并发用于此目的。

注意 launchIn 也返回一个 Job,可以用来取消对应的流收集的协程,不取消整个 scope 或 join 等待它。

流取消检查 Flow cancellation checks

为方便起见,flow 构建器对每个发出的值执行额外的 ensureActive 检查以取消。这意味着从 flow { … } 发出的繁忙循环是可以取消的。

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

fun main() = runBlocking {
    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}@ea00de

但是,出于性能原因,大多数其他流操作符不会自行进行额外的取消检查。例如,如果您使用 IntRange.asFlow 扩展函数来编写相同的繁忙循环并且不在任何地方挂起,则不会检查取消。

fun main() = runBlocking {
    (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}@1da1380b

使繁忙的流可取消 Making busy flow cancellable

如果您有一个协程内的繁忙循环,您必须明确检查取消。您可以添加 .onEach { currentCoroutineContext().ensureActive() },但是提供了一个现成的 cancellable 运算符来执行此操作。

fun main() = runBlocking {
    (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=BlockingCoroutine{Cancelled}@39ab5ef7
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值