?異步掛起函數能夠返回單一值,那么我們如何返回多個異步計算的值呢?而這個就是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,它可以用來模擬簡單轉換,就和 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
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操作符是最基礎的,除此以外還有其他操作符:
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")
}
}
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有 flatten和flatMap操作符,由于flow地異步性質,它會調用不同地flattening地模型,因此,flows有一族的flattening操作符。
flatMapConcat
?連接模式由flatMapConcat和flattenConcat操作符實現。 它們和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,因此發射地值會盡快被處理。這些由flatMapMerge和flattenMerge來實現。它們都有一個可選的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收集協程。