[譯] Kotlin Asynchronous Flow - 異步流

?異步掛起函數能夠返回單一值,那么我們如何返回多個異步計算的值呢?而這個就是Kotlin Flow需要解決地。

Representing multiple values

?在kotlin,多個值可以由Collections表示。

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

以上代碼輸出如下:

1
2
3

Sequences

?如果我們使用CPU消費型阻塞代碼生產numbers,我們可以使用Sequences表示這些numbers。

fun foo(): 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() {
    foo().forEach { value -> println(value) } 
}

?以上代碼會每隔100ms,打印出一個數字。

Suspending functions

?以上代碼會阻塞主線程,我們可以給函數添加suspend修飾符來實現異步計算。

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

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

?使用List<Int>返回類型意味著,我們需要一次性返回所有值。為了表示異步計算的值的流,我們可以使用Flow<Int>,就像之前使用Sequence<Int>同步計算值一樣。

fun foo(): Flow<Int> = flow { // flow builder
    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
    foo().collect { value -> println(value) } 
}

輸出如下:

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

Flow和之前例子的差別:

  • Flow 類型 builder 函數稱為 flow
  • flow{}內部代碼是可以掛起的
  • 函數 foo()不再需要suspend修飾符
  • 使用emit函數發射值
  • 使用collect函數收集值

我們可以使用Thread.Sleep 來替換 delay,那么當前Main Thread就會被阻塞

Flows are cold

?Flow是和sequence一樣的code stream,在flow內的代碼塊只有到flow開始被collected時才開始運行;

     
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

?這個是返回flow的foo()函數不需要被標記為suspend的關鍵理由.foo()函數會馬上返回不會有任何等待,flow每次都是在collect調用之后才開始執行,這就是為什么我們在調用collect之后才打印出來 "Flow started"。

Flow cancellation

?Flow堅持使用通用的協作式協程取消方式。flow底層并沒有采用附加的取消點。對于取消這是完全透明的。和往常一樣,如果flow是在一個掛起函數內被掛起了,那么flow collection是可以被取消的,并且在其他情況下是不能被取消的。

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 builders

?在上述例子中,flow { }構造者是最簡單的。以下還有更簡單的實現:

  • flowOf定義了一個flow用來emit一個固定的集合;
  • 各種collections和sequences都可以通過asFlow()函數來轉換為flows;
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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

Intermediate flow operators

?Flows可以通過操作符來進行轉換,就如同你使用collections和sequences一樣。中間操作符用來應用于上游flow任何產生下游flow。這些操作符都是冷啟動的,就像flows一樣。對于這些操作符的調用也都不是掛起函數。它工作很快,返回新的轉換的flow的定義。
?基礎操作符也有著和map和filter類似的名字。和sequences一個很重要的差別是在這些操作符內部可以調用掛起函數。
?舉個例子,一個請求flow可以通過map操作符映射為result,即便這個請求是在一個掛起函數內需要長時間的運行。

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
        .map { request -> performRequest(request) }
        .collect { response -> println(response) }
}

輸出如下:

response 1
response 2
response 3

Transform operator

?在flow轉換操作符中,最經常使用的就是transform,它可以用來模擬簡單轉換,就和 mapfilter一樣,也可以用來實現更加復雜地轉換。可以每次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

Size-limiting operators

?Size-limiting 中間操作符會比如take會取消flow繼續執行,當設置地limit已經達到了設定值。協程取消總是會拋出一個異常,所以所有的資源管理函數對于取消操作都會添加比如try{}finally{}代碼塊。

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

?對于flows的末端操作符都是開始flow采集的掛起函數。collect操作符是最基礎的,除此以外還有其他操作符:

  • toListtoSet.可以作集合轉換
  • 操作符可以獲取第一個值并且確保flow只會emit一個值
  • 使用reducefold 把flow 簡化合并為一個值
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking<Unit> {

   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

?每一個flow集合都是順序執行,除非應用了某個特定地針對多個flow執行地操作符。每一個值都是經過中間操作符,從上游到達下游,最終到達末端操作符。

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

fun main() = runBlocking<Unit> {

    (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 collection總是會在調用者協程發生。舉個例子,如果有一個foo flow,那么以下代碼會在作者指定地上下文中執行,而不用去看foo flow的具體執行細節。

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

?因此,flow{}內的代碼是運行在collector指定的上下文中。

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") } 
}      
[main @coroutine#1] Started foo flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3

?因為foo().collect是在主線程中被調用的。foo的flow內部代碼也是在主線程中執行。這是個完美地實現,解決快速運行或者異步代碼,不用關心執行環境和阻塞調用者線程。

Wrong emission withContext

?然而,長時間運行CPU消費型任務需要在Dispatchers.Default中執行,UI更新代碼需要在Dispatchers.Main中執行。通常,withContext用來切換kotlin 協程的上下文。但是flow{}內部的代碼必須要尊重上下文保留屬性,并且不允許從不同的上下文emit值。

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

?以下展示正確切換flow上下文的方式:

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
    }
}.flowOn(Dispatchers.Default) // RIGHT way to change context for CPU-consuming code in flow builder

fun main() = runBlocking<Unit> {
    foo().collect { value ->
        log("Collected $value") 
    } 
} 
[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改變了flow本身的順序執行上下文。collection發生在協程"coroutine#1"中,emission發生在協程"coroutine#2"并且是運行咋另一個線程中,和collect操作是同時進行地。當必須要為上下文改變CoroutineDispatcher時,flowOn操作符就會為上游flow創建一個新協程。

Buffering

?在不同協程中運行flow的不同部分,在整體立場上是非常有幫助的,特別是涉及到長時間運行的異步操作。舉個例子,當foo()的flow的emission操作比較慢,比如沒100ms生產一個一個element,并且collector也比較慢,花費300ms去處理一個元素。讓我門看看一個處理三個數字的flow需要多少時間:

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

輸出如下,整個collection操作需要花費大概1200ms。

1
2
3
Collected in 1220 ms

?我們可以使用 buffer操作符去并發執行foo()方法里的emit代碼段,然后順序執行collection操作。

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

?上述方式會更快生產numbers,因為我們有效創建了處理流程,只需要在第一個數字上等待100ms,然后每個數字都花費300ms去做處理。這樣方式會花費大概1000ms。

1
2
3
Collected in 1071 ms

注意: flowOn操作符使用了相同的緩存機制,但是它必須切換CoroutineDispatcher,但是在這里,我們顯示請求了buffer而不是切換執行上下文。

Conflation

?當一個flow表示操作的部分結果或者操作狀態更新,它可能并不需要去處理每一個值,但是需要處理最近的一個值。在這種場景下, conflate操作符可以被用于忽略中間操作符,當collector處理太慢。

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

?從上面可以看出來,第一個數字會被處理,第二第三個也會被處理,而第二個數字會被合并,只有第三個數字發送給collector進行處理。

1
3
Collected in 758 ms

Processing the latest value

? Conflation是一種對emitter和collector慢處理的一種加速方式。它通過丟棄一些值來做實現。另外一種方式就是通過取消一個慢處理collector然后重啟collector接受已經發射的新值。有一族的xxxlatest操作符來執行和xxx操作符同樣的基本邏輯。取消emit新值的代碼塊內的代碼。我們把上個例子中的conflate改成collectlatest。

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

?因為collectLatest花費來300ms,每次發送一個新值都是100ms。我們看見代碼塊都是運行在新值上,但是只會以最新值完成。

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

Composing multiple flows

有不少方式來組合多個flow。

?就像kotlin標準庫內的Sequence.zip擴展函數一樣,flows有zip操作符來組合不同的flows的值。

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

fun main() = runBlocking<Unit> { 

    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表示操作或者變量的最近值,它可能需要去執行計算根據所依賴的相應的flow的最近的值,并且會重新計算,當上游flow發射新值的時候。相應的操作符族被稱作combine

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.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替換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

Flattening flows

?Flows表示異步值接收序列,它會很容易地觸發請求另一個sequence值。比如,以下例子返回了兩個500ms間隔字符串地flow。

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

?現在我們有三個整型的flow,我們調用requestFlow來請求值。

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

?現在我們結束flows的每一個flow,需要將其扁平化為一個flow來進行進一步地處理。Collections和sequences有 flattenflatMap操作符,由于flow地異步性質,它會調用不同地flattening地模型,因此,flows有一族的flattening操作符。

flatMapConcat

?連接模式由flatMapConcatflattenConcat操作符實現。 它們和sequence是最相似的。在收集新值之前,它們會等待內部flow完成,如同下面的例子描述地一樣:

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

可以看出來 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

另一個flattening mode是并發收集flows并且將合并它們的值為一個單一flow,因此發射地值會盡快被處理。這些由flatMapMergeflattenMerge來實現。它們都有一個可選的concurrency參數來限制并發同時進行收集的flow個數。

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

可以看出來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) } )但是并發收集結果flows,它等價于順序執行map { requestFlow(it) },然后對于結果調用flattenMerge

flatMapLatest

?flatMapLatest和collectLatest操作符很像,在"Processing the latest value"章節有介紹,里面有關于"Latest"模式的介紹,只有新flow發射了了新值,那么上個flow就會被取消,由 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 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

flatMapLatest取消了代碼快內所有代碼(在這個例子中是{ requestFlow(it) })當flow發射新值。在這個特定例子中沒有差別,因為對requestFlow的調用是非常快的,非掛起也不能取消。如果我們使用掛起函數,比如delay就會按照期望的來顯示。

Flow exceptions

?如果在emitter內部或者操作符內部拋出一個異常,flow collection也是可以正常完成。也有好幾種處理異常的方式。

Collector try and catch

?在collector內部使用try/catch代碼塊來處理異常。

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

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

?這段代碼成功捕捉了 collect 末端操作符內的異常,在拋出異常之后就沒有再發送新值。

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

Everything is caught

?上個例子確實捕捉了emitter,中間操作符,末端操作符內的異常。我們修改以下代碼,將emitter發射值通過mapped修改為strings,但是除了異常之外。

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

Exception transparency

?但是怎么樣才能將封裝處理emitter代碼異常處理邏輯呢?
?Flow必須對異常處理透明,在flow{}內使用try/catch違背了異常處理透明化原則。在上述例子中,保證了從collector拋出異常也能在try/catch內捕獲。
?emitter可以使用catch操作符來實現異常透明化處理,運行異常處理封裝。catch操作符可以分析異常,根據不同的異常作出相應處理。

  • 可以使用throw再次拋出異常
  • 異常可以在catch內轉換為發射值
  • 異常可以被忽略、打印、或者由其他邏輯代碼進行處理
    ?舉個例子,我們在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> {
    foo()
        .catch { e -> emit("Caught $e") } // emit on exception
        .collect { value -> println(value) }
}

Transparent catch

?catch中間操作符,履行了異常透明化原則,只是捕捉上游異常(僅僅會捕獲catch操作符以上的異常,而不會捕獲catch以下的異常),如果是在collect{}代碼塊內的異常則會逃逸。

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

Catching declaratively

?我們可以結合 catch操作符屬性然后如果要求去處理所有異常的話就可以把 collect函數體內的邏輯轉移到onEach并且放到catch操作符前面去。這樣的Flow Collection 必須由對collect的調用觸發并且collect方法沒有參數。

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

?現在我們可以看見"Caught …"消息打印出來,現在就可以捕獲所有異常而不用顯示編寫try/catch代碼塊。

Flow completion

?在flow collection完成之后(正常完成或者異常完成)可能需要執行一個操作,可以通過兩種方式來完成: imperative 或 declarative。

Imperative finally block

?對于try/catch,collector也可以通過使用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")
    }
}  
1
2
3
Done

Declarative handling

?對于declarative方式,flow有一個onCompletion中間操作符,在flow完成collect操作后就會被調用。

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

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

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

?對于 onCompletion的關鍵性優點則是可以為空的Throwable參數,可以以此判斷flow是正常完成還是異常完成,在以下例子中,foo() flow在emit數字1之后拋出來異常。

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

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

輸入如下

1
Flow completed exceptionally
Caught exception

?onCompletion并不像catch一樣,它不會處理異常。正如我們上面看見的一樣,異常依然向下游流動,被進一步分發到onCompletion操作符,也可以在catch操作符內做處理。

Upstream exceptions only

?和catch操作符一樣,onCompletion只會看見來自上游的異常,不能看見下游異常。

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

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) 
        }
}
1
Flow completed with null
Exception in thread "main" java.lang.IllegalStateException: Collected 2

Imperative versus declarative

?現在我們知道如何去收集flow,處理completion和exceptions以imperative和declarative方式。

Launching flow

?在一些數據源,使用flows來表示異步事件是非常簡單地。我們需要和addEventListener一樣的類似處理方式,注冊一段代碼用來處理新事件和進行進一步的工作。onEach正好能服務這一角色。然而,onEach是一個中間操作符。我們也需要一個末端操作符來收集操作符。其他情況下調用onEach則毫無影響。
?如果我們在 collect末端操作符之前使用onEach,在那之后的代碼將會等待直到flow收集完成。

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") }
        .collect() // <--- Collecting the flow waits
    println("Done")
} 
Event: 1
Event: 2
Event: 3
Done

?launchIn操作符是很方便地。使用collect來替換launchIn,我們就可以在一個獨立協程中執行flow采集,所以后面的代碼就會馬上執行。

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) // <--- Launching the flow in a separate coroutine
    println("Done")
}   
Done
Event: 1
Event: 2
Event: 3

? launchIn參數可以指定一個 CoroutineScope,用來表示flow收集的協程所在的作用域。在上述例子中,這個作用域來自于 runBlocking協程構造器,runBlocking作用域會等待內部子作用域完成,在這例子中保持主函數的返回和介紹。
?在實際應用程序中,一個作用域會來自一個有限生命周期的實體。只要這個實體的生命周期終結了,那么相應的作用域也會被取消,取消相應flow的采集工作。在這種方式下,使用onEach { ... }.launchIn(scope)和addEventListener就很類似,然而,沒有必要直接調用removeEventListener,因為取消操作和結構化并發會自動完成這個操作。
?請注意,launchIn會返回 Job,在沒有取消整個作用域或join時,job可以用來cancel相應的flow收集協程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容