-
流完成
當流收集完成時(普通情況或異常情況),它可能需要執行一個動作。 你可能已經注意到,它可以通過兩種方式完成:命令式或聲明式。
命令式 finally 塊
fun simple(): Flow<Int> = (1..3).asFlow()
fun main() = runBlocking<Unit> {
try {
simple().collect { value -> println(value) }
} finally {
println("Done")
}
}
1
2
3
Done
除了 try/catch 之外,收集器還能使用 finally 塊在 collect 完成時執行一個動作。
聲明式處理
[onCompletion]的主要優點是其 lambda 表達式的可空參數 Throwable
可以用于確定流收集是正常完成還是有異常發生。在下面的示例中 simple
流在發射數字 1 之后拋出了一個異常
fun simple(): Flow<Int> = flow {
emit(1)
throw RuntimeException()
}
fun main() = runBlocking<Unit> {
simple()
.onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
.catch { cause -> println("Caught exception") }
.collect { value -> println(value) }
}
1
Flow completed exceptionally
Caught exception
completed 操作符與 catch 不同,它不處理異常。我們可以看到前面的示例代碼,異常仍然流向下游。它將被提供給后面的 onCompletion
操作符,并可以由 catch
操作符處理。
成功完成
與 [catch] 操作符的另一個不同點是 [onCompletion]能觀察到所有異常并且僅在上游流成功完成(沒有取消或失敗)的情況下接收一個 null
異常。
fun simple(): Flow<Int> = (1..3).asFlow()
fun main() = runBlocking<Unit> {
simple()
.onCompletion { cause -> println("Flow completed with $cause") }
.collect { value ->
check(value <= 1) { "Collected $value" }
println(value)
}
}
我們可以看到完成時 cause 不為空,因為流由于下游異常而中止:
1
Flow completed with java.lang.IllegalStateException: Collected 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
at FileKt$main$1$invokeSuspend$$inlined$collect$1.emit (Collect.kt:135)
at kotlinx.coroutines.flow.FlowKt__BuildersKt$asFlow$$inlined$unsafeFlow$9.collect (SafeCollector.common.kt:115)
at kotlinx.coroutines.flow.FlowKt__EmittersKt$onCompletion$$inlined$unsafeFlow$1.collect (SafeCollector.common.kt:114)
命令式還是聲明式
現在我們知道如何收集流,并以命令式與聲明式的方式處理其完成及異常情況。 這里有一個很自然的問題是,哪種方式應該是首選的?為什么? 作為一個庫,我們不主張采用任何特定的方式,并且相信這兩種選擇都是有效的, 應該根據自己的喜好與代碼風格進行選擇。
-
啟動流
使用流表示來自一些源的異步事件是很簡單的。 在這個案例中,我們需要一個類似 addEventListener
的函數,該函數注冊一段響應的代碼處理即將到來的事件,并繼續進行進一步的處理。onEach 操作符可以擔任該角色。 然而,onEach
是一個過渡操作符。我們也需要一個末端操作符來收集流。 否則僅調用 onEach
是無效的。
如果我們在 onEach
之后使用 collect 末端操作符,那么后面的代碼會一直等待直至流被收集:
// 模仿事件流
fun events(): Flow<Int> = (1..3).asFlow().onEach { delay(100) }
fun main() = runBlocking<Unit> {
events()
.onEach { event -> println("Event: $event") }
.collect() // <--- 等待流收集
println("Done")
}
Event: 1
Event: 2
Event: 3
Done
末端操作符可以在這里派上用場。使用 launchIn 替換 collect 我們可以在單獨的協程中啟動流的收集,這樣就可以立即繼續進一步執行代碼:
un main() = runBlocking<Unit> {
events()
.onEach { event -> println("Event: $event") }
.launchIn(this) // <--- 在單獨的協程中執行流
println("Done")
}
Done
Event: 1
Event: 2
Event: 3
launchIn
必要的參數 CoroutineScope 指定了用哪一個協程來啟動流的收集。在先前的示例中這個作用域來自 runBlocking 協程構建器,在這個流運行的時候,runBlocking 作用域等待它的子協程執行完畢并防止 main 函數返回并終止此示例。
在實際的應用中,作用域來自于一個壽命有限的實體。在該實體的壽命終止后,相應的作用域就會被取消,即取消相應流的收集。這種成對的 onEach { ... }.launchIn(scope)
工作方式就像 addEventListener
一樣。而且,這不需要相應的 removeEventListener
函數, 因為取消與結構化并發可以達成這個目的。
注意,launchIn 也會返回一個 Job,可以在不取消整個作用域的情況下僅取消相應的流收集或對其進行 join。
-
流取消檢測
為方便起見,[流]構建器對每個發射值執行附加的 [ensureActive]檢測以進行取消。 這意味著從 flow { ... }
發出的繁忙循環是可以取消的:
fun foo(): Flow<Int> = flow {
for (i in 1..5) {
println("Emitting $i")
emit(i)
}
}
fun main() = runBlocking<Unit> {
foo().collect { value ->
if (value == 3) cancel()
println(value)
}
}
僅得到不超過 3 的數字,在嘗試發出 4 之后拋出
Emitting 1
1
Emitting 2
2
Emitting 3
3
Emitting 4
Exception in thread "main" kotlinx.coroutines.JobCancellationException: BlockingCoroutine was cancelled
at kotlinx.coroutines.JobSupport.cancel (JobSupport.kt:1578)
at kotlinx.coroutines.CoroutineScopeKt.cancel (CoroutineScope.kt:287)
at kotlinx.coroutines.CoroutineScopeKt.cancel$default (CoroutineScope.kt:285)
參考: http://www.kotlincn.net/docs/reference/coroutines/flow.html#%E5%BC%82%E6%AD%A5%E6%B5%81