Kotlin协程和在Android中的使用总结(五 Collection、Sequence、Flow与RxJava对比(下))

在这里插入图片描述

flow的扁平化

假设现在有这样一个调用:

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

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

这时候就会获得一个这样的flow Flow<Flow<String>>,在我们最终处理的时候是要压扁成一个单独的Flow<String>,集合和序列为此具有flattenflatMap运算符。 但是,由于流flow的异步性质,它们要求使用不同的扁平化模式,因此,在流flow上有一系列扁平化运算符。

flatMapConcat

串联模式由flatMapConcat和flattenConcat运算符实现。 它们是相应Sequence运算符的最直接类似物。 他们等待内部流程完成,然后开始收集下一个示例,如以下示例所示:

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") 
    } 

从输出中可以清楚地看到flatMapConcat的顺序性质:

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

flatMapMerge

另一种展平模式是同时收集所有传入流并将其值合并为单个流,以便尽快发出值。 它由flatMapMerge和flattenMerge运算符实现。 它们都接受一个可选的并发参数,该参数限制了同时收集的并发流的数量(默认情况下它等于16)。

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") 
    } 

flatMapMerge的并发本质是显而易见的:

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

请注意,flatMapMerge顺序调用其代码块(在此示例中为*{ requestFlow(it) }),但同时并发收集结果流,这等效于先执行顺序的map{requestFlow(it)},然后对结果调用flattenMerge*

flatMapLatest

如同在前文介绍collectLatest操作符时一样,flatMapLatest操作符在展平flow时的逻辑也是一样,即每当flow发射一个新值时,如果当前collector还未处理完,则取消执行,直接执行新发射的值,即:

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") 
    } 

flatMapLatest操作符的工作原理如下log所示:

1: First at 142 ms from start
2: First at 322 ms from start
3: First at 425 ms from start
3: Second at 931 ms from start


Flow exceptions

当发射器emitter或操作符中的代码抛出异常时,流flow也会以异常而完成。 有几种处理这些异常的方法。

使用try-catch捕获collector的异常

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

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

collector发生异常后会停止flow的处理,捕获异常后输出log如下:

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

flow中的任何异常都可以被捕获

通过try-catch,除了可以捕获collector抛出的异常,对于中间操作符和末端操作符产生的异常,都可以捕获,如下代码在中间操作符里产生了异常:

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")
    } 
}   

异常被捕获,且collector流程结束,log如下:

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

Exception transparency

Flows需要保证异常透明性。也就是说不能只像上面代码中那样,把整个flow的发射和收集逻辑全部包在try-catch中,需要对flow的发射部分单独处理好异常处理逻辑,这样collector就不用再关心处理之前发生的异常情况。

这通过使用中间操作符catch来对flow的发射部分进行异常捕获处理,可以在catch块中根据捕获的异常进行不同的处理,包括以下几种处理方式:

  • 可以通过throw再次抛出异常
  • 可以通过emit将异常转换为发射值
  • 可以忽略异常,或者log打印,或者使用其他代码处理逻辑

如下将异常转换为发射值:

foo()
    .catch { e -> emit("Caught $e") } // emit on exception
    .collect { value -> println(value) }

log跟使用try-catch包裹全部代码的结果一样:

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

catch操作符保证了flow的异常透明性,但是只是对catch块之上的flow流有效,对于collect流程产生的异常无法处理,如下代码在collect中产生异常,将会抛出异常:

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) 
        }
} 

为了替代flow整个嵌套try-catch,即想保证flow的异常透明性,又能够使得collect流程中的异常也能够被捕获处理,可以将collect流程中的逻辑转移到catch之前处理,比如通过onEach操作符在catch前处理完flow中的异步序列值:

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

log输出将和上面的一样,异常将会被捕获:

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

Flow completion

flow流在正常结束或者产生异常时结束时可能需要执行一个操作,我们可以使用try-catch-finally来添加操作,也可以使用onCompletion操作符,其内部有一个可空的Throwable类型参数用以判断当前是正常结束还是异常结束,当然这个判断只能针对onCompletion操作符之前的流程,且onCompletion操作符不会捕获异常,异常将继续向下传播,如下代码:

foo()
    .onCompletion { println("Done") }
    .collect { value -> println(value) }
fun foo(): Flow<Int> = flow {
    emit(1)
    throw RuntimeException()
}

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

上面的代码将输出log:

1
Flow completed exceptionally
Caught exception

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

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

上面的代码中,onCompletion之前流程没有产生异常,所以cause为null,collect中产生的异常将仍然会被抛出,log如下:

1
Flow completed with null
Exception in thread “main” java.lang.IllegalStateException: Collected 2


Launching flow

在上面的代码中,我们在一个*runBlocking { }*块的内部执行flow的处理流程,也就是在一个协程当中处理,这使得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")
} 

将输出log:

Event: 1
Event: 2
Event: 3
Done

如果我们想collect后的代码能够同时执行,那么就可以使用launchIn操作符,它将flow的处理流程放入一个新建的协程中,这样其后续的代码就能立即被执行,如下代码:

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

将输出log:

Done
Event: 1
Event: 2
Event: 3

launchIn操作符传入的参数需要是一个声明了CoroutineScope 的对象,比如这里runBlocking协程构建器创建的CoroutineScope,通常这个CoroutineScope是一个声明周期有限的对象,比如是viewModelScope、lifecycleScope等,当其生命周期结束时,其内部的flow处理也会结束,这里的onEach { … }.launchIn(scope) 就可以起到一个addEventListener的作用(用一段代码来对传入事件作出相应处理,并继续进行进一步的工作)。

launchIn操作符同时返回一个Job,所以我们也可以使用cancel对flow的collect协程进行取消,或者使用join来执行该Job

Flow and Reactive Streams

flow流的设计灵感来源于Reactive Streams,比如RxJava,但是flow的主要目标是拥有尽可能简单的设计,以及使用Kotlin、友好的挂起函数支持、以及遵守结构化的并发。

虽然和其他Reactive Streams(比如RxJava)有所不同,但是flow本身也是一个Reactive Streams,所以可以使用Kotlin提供的转换库来在两者直接相互转换,比如Kotlin协程和在Android中的使用总结(三 将回调和RxJava调用改写成挂起函数)中提到的kotlinx-coroutines-rx2


参考:
https://kotlinlang.org/docs/reference/coroutines/flow.html#suspending-functions
Asynchronous development in Android: RxJava Vs. Kotlin Flow

发布了82 篇原创文章 · 获赞 86 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/105209834