Kotlin异步流

异步流

挂起函数异步返回一个值, 而 Kotlin 的异步流返回多个值, 与 RxJava 中的流类似.

序列 (Sequences)

首先介绍一个数据类型, 序列(Sequences)表示一个延迟计算( lazily evaluated )的集合. 这里延迟即意味着异步, 或者惰性求值. 要取得它的值, 迭代它即可, 它有返回一个迭代器 (Iterator) 的方法, 所以可以在迭代器上进行各种方便的操作, 如filter,takemap 等. 因为它是惰性求值的, 所以这个集合的元素可能是无限的. 那怎么创建一个序列呢? 可以通过顶层序列构建函数或者扩展函数, 非常方便.

fun foo(): Sequence<Int> = sequence { // 序列构建器
    for (i in 1..3) {
        Thread.sleep(100) // 假装我们正在计算
        yield(i) // 向迭代器产生一个值. Yields a value to the Iterator being built and suspends until the next value is requested.
    }
}

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

这里生成一个产生3个数字的序列, 然后迭代打印出来. 注意以上代码会阻塞主线程.

序列的操作根据它们的状态要求 (state requirement) 可以分为两类.

  • 无状态的 操作无状态要求, 独立地处理每一个元素, 如 map , filter 操作符, 或者操作仅要求常数个状态以处理一个元素, 如 take , drop 操作符.
  • 有状态的 操作要求大量的状态, 通常是序列中一定比例的元素.

如果序列操作返回另一个序列, 并且它是延迟计算的, 则它称为中介(intermediate)操作, 否则则称之终端(terminal)操作, 如 toListmax 是终端操作符.

注意, 有些序列只允许迭代或遍历一次, 否则会抛出异常.

挂起函数

当这些值由异步代码计算时,我们可以使用 suspend 修饰符标记函数 foo, 这样它就可以在不阻塞的情况下执行其工作并将结果作为列表返回:

import kotlinx.coroutines.* 
                           
suspend fun foo(): List<Int> {
    delay(1000) // 假装我们在这里做了一些异步的事情
    return listOf(1, 2, 3)
}

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

以上代码是不是有点 Rx 的味道了?

流 (Flows)

使用 List 结果类型,意味着我们只能一次返回所有值。 为了表示异步计算的值流(stream),我们可以使用 Flow 类型(正如同步计算值会使用 Sequence 类型):

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

              
fun foo(): Flow<Int> = flow { // 流构建器
    for (i in 1..3) {
        delay(100) // 假装我们在这里做了一些有用的事情
        emit(i) // 发送下一个值, 与 RxJava 的 onNext() 方法类似
    }
}

fun main() = runBlocking<Unit> {
    // 启动并发的协程以验证主线程并未阻塞
    launch {
        for (k in 1..3) {
            println("I'm not blocked $k")
            delay(100)
        }
    }
    // 收集这个流
    foo().collect { value -> println(value) } 
}
I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3

这段代码在不阻塞主线程的情况下每等待 100 毫秒打印一个数字。在主线程中运行一个单独的协程每 100 毫秒打印一次 “I'm not blocked” 已经经过了验证。注意与 RxJava 比较就容易理解了.

注意使用 Flow 的代码与先前示例的下述区别:

  • 名为 flow 的 Flow 类型构建器函数。
  • flow { ... } 构建块中的代码可以挂起。
  • 函数 foo() 不再标有 suspend 修饰符。
  • 流使用 emit 函数 发射 值。
  • 流使用 collect 函数 收集 值。

流是冷的

流像序列类型一样, 是冷的, 即 flow 构建器方法块中的代码在流被收集(调用collect()方法)之前是不会运行的, 即延迟计算的, 直到客户端代码真正使用流中的元素时才运行构建块中的代码. 这与 RxJava 中的流需要订阅才会执行是一样的道理.

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

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

fun main() = runBlocking<Unit> {
    println("Calling foo...")
    val flow = foo()
    println("Calling collect...")
    flow.collect { value -> println(value) } 
    println("Calling collect again...")
    flow.collect { value -> println(value) } 
}
Calling foo...
Calling collect...
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3

流的取消

如果流在一个可取消的挂起函数内挂起了, 则流的 collect() 调用是可以取消的, 否则不可取消. 这一点比 RxJava 弱, RxJava 在订阅流时返回一个订阅关系对象, 然后在这个对象上调用取消方法可以随机取消接收上游事件.

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

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

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

超时取消后, 没有再往下执行了.

流的创建

创建流可以使用前面展示的 flow { ... } 构建器方法, 还有其他简单的构建方式, 如:

fun <T> flowOf(vararg elements: T): Flow<T>
fun <T> flowOf(value: T): Flow<T>

另外集合或序列类型可以使用 .asFlow() 扩展函数把自己转换为流:

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

流的中介操作符 (Intermediate flow operators)

中介操作符作用于一个流, 返回一个新的流, 它是冷的, 如常用的操作符有 map , filter. 它与序列的重要不同是, 操作符中的代码块可以调用挂起函数.

调用挂起函数这个功能, RxJava 中类似的操作符是 flatMap, 它返回一个 Observable, 也是异步的, 所以可以实现同样的功能, 但相比之下, 显然 Kotlin 中调用挂起函数更简洁些.

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

          
suspend fun performRequest(request: Int): String {
    delay(1000) // 模拟一个长时间的异常操作
    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

想想如果用 RxJava 怎么实现以上的操作? RxJava 中使用的是 flatMap 操作符, 而这里使用的是 map 及 挂起函数, 注意比较异同! 才能体会到这里的简洁.

变换操作 (Transform operator)

在所有流的变换操作符中, 最一般的是 transform , 它可以模拟 map , filter , 也可以实现更复杂的变换, 使用它我们可以 emit 任意的值任意次.

import kotlinx.coroutines.*
import kotlinx.coroutines.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
        .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)

个数限制的操作符如 take 在个数达到要求时会取消流的继续执行. 而取消在协程中是通过抛出异常来实现的, 所以在这种情形下, 应该使用 try { ... } finally { ... } 块来包住流的执行, 以在必要时作清理善后工作, 防止资源泄漏.

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

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) }
}    
1
2
Finally in numbers

流的终端操作符 (Terminal flow operators)

流的终端操作符是挂起函数, 即它是异步的, 它的操作始于流中发射出元素组成的的集合. collect 是最基本的一个终端操作符, 还有:

  • 把流转换为各种集合的 toList , toSet 等操作符.
  • 获取流第一个元素的 first 操作符, 以及确定流只发射出单个元素的 single 操作符.
  • 聚集运算操作符: reduce , fold .
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)

流是顺序的 (sequential)

即流发射出的数据, 会顺序地流到下流.

流的上下文 (Flow context)

流的 collect 操作发生在调用者的指定的上下文(协程)中, 而与流的具体实现无关, 这个特性称为上下文保留.

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

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
           
fun foo(): Flow<Int> = flow {
    log("Started foo flow")
    for (i in 1..3) {
        emit(i)
    }
}  

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

如上, flow { ... } 里面的代码默认是运行在 collect 操作指定的上下文中, 输出可知是同一个线程名.

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

类比 RxJava , 通常订阅操作(即subscribe()调用)默认发生在客户端线程, 或者说由客户端代码指定. 而不是自作主张地自己再指定一个线程(上下文), 否则在 Kotlin 中会抛异常, 请看下面说明.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
                      
fun foo(): 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> {
    foo().collect { value -> println(value) } 
}      

以上代码, 在 flow { ... } 里面自己指定了一个 Dispatchers.Default 上下文. 这违背了上下文保留的特性, 所以运行以上代码会报错, 输出如下:

Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
        Flow was collected in [CoroutineId(1), "coroutine#1":BlockingCoroutine{Active}@5511c7f8, BlockingEventLoop@2eac3323],
        but emission happened in [CoroutineId(1), "coroutine#1":DispatchedCoroutine{Active}@2dae0000, DefaultDispatcher].
        Please refer to 'flow' documentation or use 'flowOn' instead
    at ...

flowOn 操作符 (指定订阅上下文)

上一节说过, 发射数据的线程默认与在客户代码相同, 但是如果确实需要切换到另一个不同的线程呢? 可以使用 flowOn 操作符.

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

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
           
fun foo(): 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
    }
} // RIGHT way to change context for CPU-consuming code in flow builder

fun main() = runBlocking<Unit> {
    foo().flowOn(Dispatchers.Default).collect { value ->
        log("Collected $value") 
    } 
} 

这里, 客户端使用 flowOn 操作符指定了流发射数据的上下文为 Dispatchers.Default , 输出可以看出, 发射数据与收集数据为不同的线程.

[DefaultDispatcher-worker-1 @coroutine#2] Emitting 1
[main @coroutine#1] Collected 1
[DefaultDispatcher-worker-1 @coroutine#2] Emitting 2
[main @coroutine#1] Collected 2
[DefaultDispatcher-worker-1 @coroutine#2] Emitting 3
[main @coroutine#1] Collected 3

flowOn 操作符与 RxJava 中调用 subscribeOn 指定订阅线程是一样的功效. 思考: RxJava 中可以指定结果的观察线程, Kotlin 流中有没有等价操作呢?

缓存操作符 (buffer)

考虑以下例子, 假设流发射一个元素很慢, 收集一个元素也很慢.

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

fun foo(): 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 {
        foo().collect { value -> 
            delay(300) // pretend we are processing it for 300 ms
            println(value) 
        } 
    }   
    println("Collected in $time ms")
}

这里统计它的运行时间, 因为流是顺序执行的, 所以这里耗时1200+ms.

1
2
3
Collected in 1220 ms

如果使用 buffer 操作符:

val time = measureTimeMillis {
    foo()
        .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")

最终输出耗时 1000+ms, 因为 buffer 操作符会创建一个新的协程来执行缓存上游元素的操作, 这个例子中它的上游发射元素比它的下游快, 所以它的下游只需要等待第一个元素发射的100ms, 然后后面就不需要再等待了, 因为已经缓存好了. 注意, 当上游执行比下游快, 缓存区将会满, 这时它将挂起上游(实际是通道(channel)的生产者), 直到下游消费速度赶上.

合并操作符 (conflate)

当一个流表示某个事件的部分结果或者事件的当前状态, 客户端下游可能并不关心它的每个状态, 相反, 它可能跳过中间态, 只关心最新的状态, 当然, 前提是下游处理状态太慢, 来不及接收每一个状态.

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

fun foo(): 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 {
        foo()
            .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个元素时, 第1个, 第3个已经来了, 所以第2个被合并/舍弃了, 只有第3个元素投递到下游.

处理最新元素的操作符 (xxxLatest)

有时我们只关注流中最新的元素, 当新元素到达时, 如果前一个元素还没有处理完, 则取消它, 并且马上处理最新的元素. 有一组 xxxLatest 操作符, 如 collectLatestmapLatest , combineLatest 等, 用于处理这种情况.

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

fun foo(): 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 {
        foo()
            .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

可以看出, 下游消费数据比上游生产数据慢, 当新数据到达时, 取消了正在处理的工作, 接手了最新的元素.

zip 操作符

zip 组合两个流中对应的两个元素, 即顺序是一对一的, 并且当最先结束的那流结束后, 处理结束.

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

fun main() = runBlocking<Unit> { 

    val nums = (1..10).asFlow() // numbers 1..10
    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 操作符

combine 也是组合多个流中的元素, 与 zip 所不同的是, 它组合的是多个流中当前最新的各个元素.

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

fun main() = runBlocking<Unit> { 

    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 452 ms from start
2 -> one at 651 ms from start
2 -> two at 854 ms from start
3 -> two at 952 ms from start
3 -> three at 1256 ms from start

这里 onEach 表示在流发射出当前元素前执行的动作, 可以看作是流发射元素前的一个回调. 从输出可以看出, 每个流发射出一个元素之后, 会拿另一个流最近发射的元素, 组合一起, 输出新的元素.

流的扁平化 (Flattening flows)

如果上游的每个元素, 导致下游消费后生产出一个新的流, 那么下游会得到一个流的流, 即流的嵌套.

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

val flowOfFlow: Flow<Flow<String>> = (1..3).asFlow().map { requestFlow(it) }

如果直接使用 Flow<Flow<String>> 类型的变量显然不方便, 能不能把它变成 Flow<String> 类型 , 去掉嵌套呢? 这个问题就是流的扁平化.

flatMapConcat 操作符

flatMapConcat 连接多个流, 使之扁平化, 它等待每一个内嵌的流结束后, 才会去 collect 下一个内嵌流.

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 = 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 121 ms from start
1: Second at 622 ms from start   // 内嵌流结束
2: First at 727 ms from start
2: Second at 1227 ms from start  // 内嵌流结束
3: First at 1328 ms from start
3: Second at 1829 ms from start  // 内嵌流结束

这个操作符其实等价于map(transform).flattenConcat(), 其中 flattenConcat() 的定义如下:

fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T>

flatMapMerge 操作符

flatMapMerge 与 flatMapConcat 一样都是扁平化流, 不同的是, 它可以并发地(可以通过concurrency参数指定并发数) collect 多个流, 并且一旦其中一个有发射出元素时, 它都马上转发给下游.

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 = 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 136 ms from start
2: First at 231 ms from start
3: First at 333 ms from start
1: Second at 639 ms from start
2: Second at 732 ms from start
3: Second at 833 ms from start

这个操作符其实等价于map(transform).flattenMerge(concurrency), 其中 flattenMerge 的定义如下:

fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> 

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 = 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 147 ms from start
2: First at 283 ms from start
3: First at 384 ms from start
3: Second at 885 ms from start

注意, 这里取消的是整个 { requestFlow(it) } 代码块的执行, 即整个内嵌流, 而不是内嵌流里的单个元素.

流的异常处理

流发射元素或者操作符变换元素时, 可能抛出异常, 这会导致流在 collect 时以异常结束. 所以 try ... catch ... 住流的创建与收集操作即可.

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

fun foo(): 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 {
        foo().collect { value -> println(value) }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}   
Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: Crashed on 2

异常后, 流不再发射数据.

异常透明

对流来说, 异常对它来说应该是透明的, 即异常不应该由它来处理. 流发射数据的代码不应被 try...catch... 包裹着, 否则它违反了异常透明的原则. 异常透明保证了收集数据(collect调用)时抛出的异常总是可以被它外围的 try...catch... 块捕获.

如果要处理流发射数据时的异常, 可以使用 catch 操作符, 它接收到异常, 可以这样处理:

  • 重新抛出异常
  • 转换为一个数据后使用 emit 发射出去
  • 忽略异常, 记录为日志, 或者交由其他代码处理
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun foo(): 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> {
    foo()
        .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

catch 操作符只能捕获它上游的异常, 而无法捕获它下游的.

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

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

fun main() = runBlocking<Unit> {
    foo()
        .catch { e -> println("Caught $e") } // does not catch downstream exceptions
        .collect { value ->
            check(value <= 1) { "Collected $value" }  // 无法捕获这里的异常         
            println(value) 
        }
} 

如果不想使用 try...catch... 块包住collect调用, 可以这么做, 把 collect 块的逻辑放到 onEach 块中, 然后后接一个 catch 操作符处理所有异常, 最后调用无参数的 collect 方法. 这种方式很像 RxJava 中的 onError 回调模式.

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

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

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

流的结束

如果流结束后, 无论是正常结束还是异常导致的结束, 希望执行一些行为, 应该怎么做? 有两种方法:

使用 finally 块, 如:

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

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

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

或者使用 onCompletion 操作符:

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

注意, onCompletion 中的 cause 如果为 null , 说明是正常结束, 否则异常结束, 并且异常会传播到下游, 下游也可以使用 catch 操作符捕获. 另外, onCompletion 也只可以看到它上游的异常, 而无法看到它下游的异常.

启动流

前面说过, 除了在 collect 中处理接收到流的元素外, 也可以使用 onEach 操作符注册自己的处理动作, 然后使用一个不带参数的 collect 启动流.

import kotlinx.coroutines.*
import kotlinx.coroutines.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") }  //相当于注册了一个监听, addListener
        .collect() // <--- Collecting the flow waits
    println("Done")
}   
Event: 1
Event: 2
Event: 3
Done

但是, 这种方法是会阻塞 collect 之后的代码的, 除非另起一个协程, 而 launchIn 操作符可以简化启动流, 并且另起一个协程这些操作. 另外launchIn 须带一个 CoroutineScope 参数, 表示在哪个线程收集流, 这样 CoroutineScope 也提供了生命周期控制的结构化并发的能力, 即无须调用 removeListener之类的清理方法防止内存泄漏.

import kotlinx.coroutines.*
import kotlinx.coroutines.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") }
        .launchIn(this) // <--- 在一个单独的协程中启动流
    println("Done")
} 
Done
Event: 1
Event: 2
Event: 3

launchIn 返回一个 Job , 可以单独 cancel , 而无须取消整个CoroutineScope.

与 Rx 集成

Kotlin 的 Flow 在设计时受 Reactive Stream 的启发, 但它的设计原则是尽可能地简单, 并且与协程紧密相关. 另外官方提供了与其他库的集成, 如与 RxJava2 集成的扩展是 kotlinx-coroutines-rx2.

参考:
https://kotlinlang.org/docs/reference/coroutines/flow.html
https://medium.com/@elizarov/reactive-streams-and-kotlin-flows-bfd12772cda4

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/112425767