Kotlin coroutines and the use summary in Android (five Collection, Sequence, Flow and RxJava comparison (below))

Insert picture description here

flow flattening

Suppose now that there is such a call:

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

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

At this time, you will get a flow like this Flow<Flow<String>>. When we finally deal with it, we will squash it into a single one Flow<String>. The collection and sequence have flatten and flatMap operators for this purpose . However, due to the asynchronous nature of flow, they require different flattening modes. Therefore, there is a series of flattening operators on flow.

flatMapConcat

The concatenation mode is implemented by the flatMapConcat and flattenConcat operators. They are the most direct analogues of the corresponding Sequence operators. They wait for the internal process to complete and then start collecting the next example, as shown in the following example:

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

The order nature of flatMapConcat can be clearly seen from the output:

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

Another flattening mode is to collect all incoming streams at the same time and merge their values ​​into a single stream so that the values ​​are sent out as soon as possible. It is implemented by the flatMapMerge and flattenMerge operators. They both accept an optional concurrency parameter, which limits the number of concurrent streams that are collected simultaneously (by default it is equal to 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") 
    } 

The concurrency nature of flatMapMerge is obvious:

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

Please note that flatMapMerge calls its code blocks sequentially (in this example, * {requestFlow (it)} ), but at the same time collects the result flow concurrently, which is equivalent to executing the sequenced map {requestFlow (it)} first , and then the result Call flattenMerge *

flatMapLatest

As in the previous introduction to the collectLatest operator, the logic of the flatMapLatest operator when flattening flow is the same. That is, whenever the flow emits a new value, if the current collector has not been processed, the execution is canceled and the newly launched Value, that is:

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

The working principle of the flatMapLatest operator is shown in the following 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

When the code in the emitter emitter or operator throws an exception, the flow will also be completed with an exception. There are several ways to deal with these exceptions.

Use try-catch to catch the exception of the 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")
    } 
}  

The collector will stop the flow processing after an exception occurs, and the log output after the exception is captured as follows:

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

Any anomalies in flow can be caught

Through try-catch, in addition to catching exceptions thrown by the collector, exceptions generated by the intermediate operator and the terminal operator can be captured. The following code generates an exception in the intermediate operator:

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

The exception is caught and the collector process ends, the log is as follows:

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

Exception transparency

Flows need to ensure exceptional transparency. In other words, you can't just wrap the entire flow emission and collection logic in the try-catch as in the above code. You need to handle the exception handling logic separately for the flow emission part, so that the collector no longer needs to care about what happened before processing abnormal situation.

This uses the intermediate operator catch to perform exception capture processing on the emission part of the flow, and different processing can be performed according to the captured exception in the catch block, including the following processing methods:

  • You can throw an exception again by throw
  • Abnormality can be converted to emission value by emit
  • Can ignore exceptions, or log printing, or use other code processing logic

The exception is converted to the emission value as follows:

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

The log is the same as using try-catch to wrap all the code:

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

The catch operator guarantees the exception transparency of flow, but it is only valid for the flow flow above the catch block. It cannot handle the exception generated by the collect process. The following code generates an exception in the collect and will throw an exception:

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

In order to replace the entire nested try-catch of the flow, that is to ensure the transparency of the exception of the flow, and to enable the exception in the collect process to be captured and processed, the logic in the collect process can be transferred to the catch before processing, such as through the onEach operation The operator processes the asynchronous sequence values ​​in flow before catching:

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

The log output will be the same as above, and the exception will be caught:

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

Flow completion

The flow may need to perform an operation when it ends normally or when an exception occurs. We can use try-catch-finally to add an operation, or we can use the onCompletion operator, which has a nullable Throwable type parameter to determine the current end is normal or abnormal, of course, this can only be determined for onCompletion flow before the operator, and onCompletion operator does not capture the exception, this will continue to propagate down, the following code:

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

The above code will output 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) 
        }
}

In the above code, the process before onCompletion did not generate an exception, so cause is null, and the exception generated in the collect will still be thrown, the log is as follows:

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


Launching flow

In the above code, we execute the flow processing flow inside a * runBlocking {} * block, that is, in a coroutine, which makes the flow flow processing will block the execution of the following code, such as:

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

Will output log:

Event: 1
Event: 2
Event: 3
Done

If we want the code after collection to be executed at the same time, we can use the launchIn operator, which puts the flow of the flow into a new coroutine, so that its subsequent code can be executed immediately, as follows:

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

Will output log:

Done
Event: 1
Event: 2
Event: 3

The parameter passed by the launchIn operator needs to be an object that declares CoroutineScope, such as the CoroutineScope created by the runBlocking coroutine builder. Usually this CoroutineScope is an object with a limited declaration period, such as viewModelScope, lifecycleScope, etc., when its life cycle ends At this time, its internal flow processing will also end, onEach {…} .launchIn (scope) here can function as an addEventListener (use a piece of code to process the incoming event accordingly, and continue to work further) .

The launchIn operator returns a job at the same time , so we can also use cancel to cancel the flow's collect coroutine, or use join to execute the job .

Flow and Reactive Streams

The inspiration for the design of flow streams comes from Reactive Streams, such as RxJava, but the main goal of flow is to have the simplest possible design, use Kotlin, friendly suspend function support, and adhere to structured concurrency.

Although it is different from other Reactive Streams (such as RxJava), flow itself is also a Reactive Streams, so you can use the conversion library provided by Kotlin to directly convert between the two, such as Kotlin coroutine and Android use summary (three Rewrite callbacks and RxJava calls to suspend functions) as mentioned in kotlinx-coroutines-rx2 .


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

Published 82 original articles · Like 86 · Visit 110,000+

Guess you like

Origin blog.csdn.net/unicorn97/article/details/105209834