最近一直在了解關于 Kotlin協程 的知識,那最好的學習資料自然是官方提供的學習文檔了,看了看后我就萌生了翻譯官方文檔的想法。前后花了要接近一個月時間,一共九篇文章,在這里也分享出來,希望對讀者有所幫助。個人知識所限,有些翻譯得不是太順暢,也希望讀者能提出意見
協程官方文檔:coroutines-guide
掛起函數可以異步返回單個值,但如何返回多個異步計算值呢?這就是 kotlin Flows(流) 的用處了
一、表示多個值
可以使用集合在 kotlin 中表示多個值。例如,有一個函數 foo(),它返回包含三個數字的 List,然后使用 forEach 打印它們
fun foo(): List<Int> = listOf(1, 2, 3)
fun main() {
foo().forEach { value -> println(value) }
}
輸出結果:
1
2
3
1.1、序列
如果我們使用一些 CPU 消耗型 的阻塞代碼(每次計算需要100毫秒)來計算數字,那么我們可以使用一個序列(Sequence)來表示數字:
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) }
}
這段代碼輸出相同的數字列表,但每打印一個數字前都需要等待100毫秒
1.2、掛起函數
上一節的代碼的計算操作會阻塞運行代碼的主線程。當這些值由異步代碼計算時,我們可以用 suspend 修飾符標記函數 foo,以便它可以在不阻塞的情況下執行其工作,并將結果作為列表返回
import kotlinx.coroutines.*
//sampleStart
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) }
}
//sampleEnd
這段代碼在等待一秒后輸出數字
1.3、Flows
使用 List< Int > 作為返回值類型,意味著我們只能同時返回所有值。為了表示異步計算的值流,我們可以使用 Flow< Int > 類型,就像同步計算值的 Sequence< Int > 類型一樣
//sampleStart
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) }
}
//sampleEnd
此代碼在打印每個數字前等待100毫秒,但不會阻塞主線程。通過從主線程中運行的單獨協程中每隔100毫秒打印了一次 “I'm not blocked”,可以驗證這一點:
I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3
請注意,代碼與前面示例中的 Flow 有以下不同:
- Flow 類型的構造器函數名為 flow
- flow{...} 中的代碼塊可以掛起
- foo 函數不再標記 suspend 修飾符
- 值通過 emit 函數從流中發出
- 通過 collect 函數從 flow 中取值
我們可以用 Thread.sleep 來代替 flow{...} 中的 delay,可以看到在這種情況下主線程被阻塞住了
二、流是冷的
Flows 是冷流(cold streams),類似于序列(sequences),flow builder 中的代碼在開始收集流值之前不會運行。在下面的示例中可以清楚地看到這一點:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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) }
}
//sampleEnd
運行結果:
Calling foo...
Calling collect...
Flow started
1
2
3
Calling collect again...
Flow started
1
2
3
這是 foo() 函數(返回了 flow)未標記 suspend
修飾符的一個關鍵原因。foo()
本身返回很快,不會進行任何等待。flow 每次收集時都會啟動,這就是我們再次調用 collect
時會看到“flow started”的原因
三、取消流
Flow 采用和協程取同樣的協作取消。但是,Flow 實現基礎并沒有引入額外的取消點,它對于取消操作是完全透明的。通常,流的收集操作可以在當流在一個可取消的掛起函數(如 delay)中掛起的時候取消,否則不能取消
以下示例展示了在 withTimeoutOrNull 塊中流如何在超時時被取消并停止執行
//sampleStart
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")
}
//sampleEnd
注意,foo() 函數中的 Flow 只傳出兩個數字,得到以下輸出:
Emitting 1
1
Emitting 2
2
Done
相對應的,可以注釋掉 flow 中的 delay 函數,并增大 for 循環的循環范圍,此時可以發現 flow 沒有被取消,因為 flow 中沒有引入額外的掛起點
//sampleStart
fun foo(): Flow<Int> = flow {
for (i in 1..Int.MAX_VALUE) {
// delay(100)
println("Emitting $i")
emit(i)
}
}
fun main() = runBlocking<Unit> {
withTimeoutOrNull(250) {
// Timeout after 250ms
foo().collect { value -> println(value) }
}
println("Done")
}
//sampleEnd
四、流構建器
前面例子中的 flow{...} 是最基礎的一個流構建器,還有其它的構建器可以更容易地聲明流:
- flowOf() 定義了一個發出固定值集的流構建器
- 可以使用擴展函數
.asFlow()
將各種集合和序列轉換為流
因此,從流中打印從 1 到 3 的數字的例子可以改寫成:
fun main() = runBlocking<Unit> {
//sampleStart
// Convert an integer range to a flow
(1..3).asFlow().collect { value -> println(value) }
//sampleEnd
}
五、中間流運算符
可以使用運算符來轉換流,就像使用集合和序列一樣。中間運算符應用于上游流并返回下游流。這些運算符是冷操作符,和流一樣。此類運算符本身不是掛起函數,它工作得很快,其返回一個新的轉換后的流,但引用僅包含對新流的操作定義,并不馬上進行轉換
基礎運算符有著熟悉的名稱,例如 map 和 filter。流運算符和序列的重要區別在于流運算符中的代碼可以調用掛起函數
例如,可以使用 map 運算符將傳入請求流映射為結果值,即使執行請求是由掛起函數實現的長時間運行的操作:
//sampleStart
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) }
}
//sampleEnd
運行結果共有三行,每一秒打印一行輸出
response 1
response 2
response 3
5.1、轉換操作符
在流的轉換運算符中,最常用的一個稱為 transform
。它可以用來模擬簡單的數據轉換(就像 map 和 filter),以及實現更復雜的轉換。使用 transform
運算符,我們可以發出任意次數的任意值
例如,通過使用 transform
,我們可以在執行長時間運行的異步請求之前發出一個字符串,并在該字符串后面跟隨一個響應:
suspend fun performRequest(request: Int): String {
delay(1000) // imitate long-running asynchronous work
return "response $request"
}
fun main() = runBlocking<Unit> {
//sampleStart
(1..3).asFlow() // a flow of requests
.transform { request ->
emit("Making request $request")
emit(performRequest(request))
}
.collect { response -> println(response) }
//sampleEnd
}
輸出值:
Making request 1
response 1
Making request 2
response 2
Making request 3
response 3
5.2、限長運算符
限長中間運算符在達到相應限制時取消流的執行。協程中的取消總是通過拋出異常來實現,這樣所有的資源管理函數(例如 try { ... } finally { ... } )就可以在取消時正常執行
//sampleStart
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) }
}
//sampleEnd
這段代碼的輸出清楚地顯示了 numbers() 函數中的 flow{...} 函數體在發出第二個數字后就停止了:
1
2
Finally in numbers
六、流運算符
終端流運算符是用于啟動流的掛起函數。collect
是最基本的終端流運算符,但還有其它終端運算符,可以使得操作更加簡便:
- 轉換為各種集合,如 toList 和 toSet 函數
- first 運算符用于獲取第一個值,single 運算符用于確保流發出單個值
- 使用 reduce 和 fold 將流還原為某個值
例如:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
//sampleStart
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)
//sampleEnd
}
輸出單個值:
55
七、流是連續的
除非使用對多個流進行操作的特殊運算符,否則每個流的單獨集合都是按順序執行的。集合直接在調用終端運算符的協程中工作,默認情況下不會啟動新的協程。每個發出的值都由所有中間運算符從上游到下游進行處理,然后在之后傳遞給終端運算符
請參閱以下示例,該示例過濾偶數并將其映射到字符串:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
//sampleStart
(1..5).asFlow()
.filter {
println("Filter $it")
it % 2 == 0
}
.map {
println("Map $it")
"string $it"
}.collect {
println("Collect $it")
}
//sampleEnd
}
輸出:
Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5
八、流上下文
流的收集總是在調用協程的上下文中執行。例如,如果存在 foo 流,則無論 foo 流的實現詳細信息如何,以下代碼都將在該開發者指定的上下文中執行:
withContext(context) {
foo.collect { value ->
println(value) // run in the specified context
}
}
流的這個特性稱為上下文保留
所以,默認情況下,flow{...} 中的代碼在相應流的收集器提供的上下文中運行。例如,觀察 foo 的實現,它打印調用它的線程并發出三個數字:
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
//sampleStart
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") }
}
//sampleEnd
運行結果:
[main @coroutine#1] Started foo flow
[main @coroutine#1] Collected 1
[main @coroutine#1] Collected 2
[main @coroutine#1] Collected 3
由于 foo().collect
是在主線程調用的,所以 foo 流也是在主線程中調用。對于不關心執行上下文且不阻塞調用方的快速返回代碼或者異步代碼,這是完美的默認設置
8.1、錯誤地使用 withContext
但是,可能需要在 Dispatchers 的上下文中執行長時間運行的占用 CPU 的代碼,可能需要在 Dispatchers.Main 的上下文中執行默認代碼和 UI 更新。通常,withContext 用于在使用 kotlin 協程時更改代碼中的上下文,但 fow{...}
中的代碼必須遵守上下文本保留屬性,并且不允許從其它上下文中觸發
嘗試運行以下代碼:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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) }
}
//sampleEnd
代碼會生成以下異常:
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 ...
8.2、flowOn 運算符
有個例外情況,flowOn 函數能用于改變流發送值時的上下文。改變流上下文的正確方式如下面的示例所示,該示例還打印了相應線程的名稱,以顯示所有線程的工作方式:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
//sampleStart
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")
}
}
//sampleEnd
注意,flow{...} 在后臺線程中工作,而在主線程中進行取值
這里要注意的另一件事是 flowOn 操作符改變了流的默認順序性質。現在取值操作發生在協程 "coroutine#1" 中,而發射值的操作同時運行在另一個線程中的協程 "coroutine#2" 上。當必須在上游流的上下文中更改 CoroutineDispatcher 時,flowOn 運算符將為該上游流創建另一個協程
九、緩沖
從收集流所需的總時間的角度來看,在不同的協程中運行流的不同部分可能會有所幫助,特別是當涉及到長時間運行的異步操作時。例如,假設 foo() 流的發射很慢,生成元素需要100毫秒;收集器也很慢,處理元素需要300毫秒。讓我們看看用三個數字收集這樣的流需要多長時間:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*
//sampleStart
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")
}
//sampleEnd
以上代碼會產生如下類似的結果,整個收集過程大約需要1200毫秒(三個數字,每個400毫秒)
1
2
3
Collected in 1220 ms
我們可以在流上使用 buffer 運算符,在運行取集代碼的同時運行 foo() 的發值代碼,而不是按順序運行它們
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> {
//sampleStart
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")
//sampleEnd
}
這可以得到相同的輸出結果但運行速度更快,因為我們已經有效地創建了一個處理管道,第一個數字只需要等待100毫秒,然后只需要花費300毫秒來處理每個數字。這樣運行大約需要1000毫秒:
1
2
3
Collected in 1071 ms
請注意,flowOn 運算符在必須更改 CoroutineDispatcher 時使用相同的緩沖機制,但這里我們顯示地請求緩沖而不更改執行上下文
9.1、合并
當流用于表示操作或操作狀態更新的部分結果時,可能不需要處理每個值,而是只處理最近的值。在這種情況下,當取值器處理中間值太慢時,可以使用合并運算符跳過中間值。在前面的例子的基礎上再來修改下:
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> {
//sampleStart
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")
//sampleEnd
}
可以看到,雖然第一個數字仍在處理中,但第二個數字和第三個數字已經生成,因此第二個數字被合并(丟棄),只有最近的一個數字(第三個)被交付給取值器:
1
3
Collected in 758 ms
9.2、處理最新值
在發射端和處理端都很慢的情況下,合并是加快處理速度的一種方法。它通過丟棄發射的值來實現。另一種方法是取消慢速收集器,并在每次發出新值時重新啟動它。有一系列 xxxLatest 運算符與 xxx 運算符執行相同的基本邏輯,但是在新值產生的時候取消執行其塊中的代碼。在前面的示例中,我們嘗試將 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> {
//sampleStart
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")
//sampleEnd
}
由于 collectLatest 的主體需要延遲300毫秒,而每100毫秒會發出一個新值,因此我們可以看到 collectLatest 代碼塊得到了每一個發射值,但最終只完成了最后一個值:
Collecting 1
Collecting 2
Collecting 3
Done 3
Collected in 741 ms
十、組合多個流
有許多方法可以組合多個流
10.1、zip
與 Kotlin 標準庫中的 Sequence.zip 擴展函數一樣,流有一個 zip 運算符,用于組合兩個流的相應值:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
//sampleStart
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
//sampleEnd
}
運行結果:
1 -> one
2 -> two
3 -> three
10.2、Combine
當 flow 表示變量或操作的最新值時(參閱有關 conflation 的相關章節),可能需要執行依賴于相應流的最新值的計算,并在任何上游流發出值時重新計算它。相應的運算符族稱為 combine
例如,如果上例中的數字每300毫秒更新一次,但字符串每400毫秒更新一次,則使用 zip 運算符壓縮它們仍會產生相同的結果,盡管結果是每400毫秒打印一次
在本例中,我們使用中間運算符 onEach 來延遲每個元素,并使發出樣本流的代碼更具聲明性,更加簡短
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
//sampleStart
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")
}
//sampleEnd
}
但是,如果在此處使用 combine 運算符而不是 zip 時:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
//sampleStart
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")
}
//sampleEnd
}
我們得到了完全不同的輸出:
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
十一、展平流
流表示異步接收的值序列,因此在每個值觸發對另一個值序列的請求的情況下很容易獲取新值。例如,我們可以使用以下函數,該函數返回相隔500毫秒的兩個字符串流:
fun requestFlow(i: Int): Flow<String> = flow {
emit("$i: First")
delay(500) // wait 500 ms
emit("$i: Second")
}
現在,如果我們有一個包含三個整數的流,并為每個整數調用 requestFlow,如下所示:
(1..3).asFlow().map { requestFlow(it) }
然后我們最終得到一個流(flow< flow< String >>),需要將其展平為單獨一個流以進行進一步處理。集合和序列對此提供了 flatten 和 flatMap 運算符。然而,由于流的異步特性,它們需要不同的展開模式,因此流上有一系列 flattening 運算符
11.1、flatMapConcat
flatMapConcat 和 flattencat 運算符實現了 Concatenating 模式,它們是與序列運算符最直接的類比。它們等待內部流完成,然后開始收集下一個流,如下例所示:
fun requestFlow(i: Int): Flow<String> = flow {
emit("$i: First")
delay(500) // wait 500 ms
emit("$i: Second")
}
fun main() = runBlocking<Unit> {
//sampleStart
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")
}
//sampleEnd
}
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
11.2、flatMapMerge
另一種 flattening 模式是同時收集所有傳入流并將其值合并到單個流中,以便盡快發出值。它由 flatMapMerge 和 flattenMerge 運算符實現。它們都接受一個可選的并發參數,該參數用于限制同時收集的并發流的數量(默認情況下等于 DEFAULT_CONCURRENCY)
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> {
//sampleStart
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")
}
//sampleEnd
}
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
11.3、flatMapLatest
與“Processing the latest value(處理最新值)”章節介紹的 collectLatest 操作符類似,存在相應的 "Latest" flattening 模式。在該模式下,一旦發出新流,將取消先前已發出的流。這通過 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> {
//sampleStart
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")
}
//sampleEnd
}
本例中的輸出很好的演示了 flatMapLatest 的工作原理
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)})。requestFlow 函數本身的調用是很快速的,并非掛起函數,如果其內部不包含額外的掛起點,那么它就不能被取消,所以此處就在其內部使用了 delay 函數,使其可以達到被取消的目的
十二、流異常
當發射器或運算符內部的代碼引發異常時,流收集器可以結束運行,但會出現異常。有幾種方法可以處理這些異常
12.1、收集器 try 與 catch
收集器可以使用 kotlin 的 try/catch 代碼塊來處理異常
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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")
}
}
//sampleEnd
此代碼成功捕獲 collect 運算符中的異常,如我們所見,在此之后不再發出任何值:
Emitting 1
1
Emitting 2
2
Caught java.lang.IllegalStateException: Collected 2
12.2、一切都已捕獲
前面的示例實際上捕獲了發射器或任何中間或終端運算符中發生的任何異常。例如,讓我們更改代碼,以便將發出的值映射到字符串,但相應的代碼會產生異常:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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")
}
}
//sampleEnd
仍捕獲此異常并停止收集:
Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: Crashed on 2
十三、異常透明性
但是發射器的代碼如何封裝其異常處理行為呢?
flows 對于異常必須是透明的,并且在 flow{...} 構建器中發射值有可能拋出異常時,異常必須顯式地從 try/catch 塊內部拋出。這保證了拋出異常的收集器始終可以使用 try/catch 來捕獲異常,如前一個示例所示
發射器可以使用 catch 運算符來保持此異常的透明性,并允許封裝其異常處理行為。catch 運算符可以分析異常并根據捕獲到的異常以不同的方式對其作出反應:
- 可以使用 throw 重新引發異常
- 使用 catch 的 emit 可以將異常轉換為值的 emission
- 異常可以被其他代碼忽略、記錄或處理
例如,讓我們在捕獲異常時發出一段文本:
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> {
//sampleStart
foo()
.catch { e -> emit("Caught $e") } // emit on exception
.collect { value -> println(value) }
//sampleEnd
}
示例代碼的輸出結果是與之前相同的,即使我們不再在代碼周圍使用 try/catch
13.1、透明捕獲
catch 中間運算符遵循異常透明性,只捕獲上游異常(即 catch 上所有運算符的異常,而不是 catch 下所有運算符的異常)。如果 collect{...}(放在 catch 下面)拋出異常,程序將退出:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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)
}
}
//sampleEnd
盡管存在 catch 運算符,但不會打印 “Caught ...” 日志
13.2、聲明式捕獲
我們可以將 catch 運算符的聲明性與處理所有異常的愿望結合起來,方法是將 collect 運算符原先所要做的操作移動到 onEach 中,并將其放在 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> {
//sampleStart
foo()
.onEach { value ->
check(value <= 1) { "Collected $value" }
println(value)
}
.catch { e -> println("Caught $e") }
.collect()
//sampleEnd
}
現在我們可以看到打印了一條 “Caught ...” 消息,至此我們捕獲了所有異常,而無需顯式使用 try/catch
十四、流完成
當流收集完成時(正常或異常),它可能需要執行一個操作。正如你可能已經注意到的,它可以通過兩種方式完成:命令式或聲明式
14.1、命令式 finally 塊
除了 try/catch 外,收集器還可以使用 finally 在收集完成時執行操作
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
fun foo(): Flow<Int> = (1..3).asFlow()
fun main() = runBlocking<Unit> {
try {
foo().collect { value -> println(value) }
} finally {
println("Done")
}
}
//sampleEnd
此代碼打印 fon() 流生成的三個數字,之后跟隨 "Done" 字符串
1
2
3
Done
14.2、聲明式處理
對于聲明性方法,flow 有一個 onCompletion 中間運算符,該運算符在流完全收集后調用
前面的示例可以使用 onCompletion 運算符重寫,并生成相同的輸出:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun foo(): Flow<Int> = (1..3).asFlow()
fun main() = runBlocking<Unit> {
//sampleStart
foo()
.onCompletion { println("Done") }
.collect { value -> println(value) }
//sampleEnd
}
onCompletion 的主要優點是包含一個 lambda 參數,該 lambda 包含一個可空的 Throwable 參數,該 Throwable 參數可用于確定流收集是正常完成還是異常完成。在以下示例中,foo() 流在發出數字1后引發異常:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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) }
}
//sampleEnd
如你所料,將打印:
1
Flow completed exceptionally
Caught exception
與 catch 運算符不同,onCompletion 運算符不處理異常。正如我們從上面的示例代碼中看到的,異常仍然會流向下游。它將被傳遞給其他完成 onCompletion 運算符,并可以使用 catch 運算符進行處理
14.3、僅限上游異常
就像 catch 操作符一樣,onCompletion 只看到來自上游的異常,而看不到下游的異常。例如,運行以下代碼:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
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)
}
}
//sampleEnd
我們可以看到 completion cause 為空,但流收集失敗并拋出異常:
1
Flow completed with null
Exception in thread "main" java.lang.IllegalStateException: Collected 2
十五、命令式還是聲明式
現在我們知道如何收集流,并以命令式和聲明式的方式處理它的完成和異常。這里很自然的就有了個問題,應該首選哪種方法呢?為什么?作為一個庫,我們不提倡任何特定的方法,并且相信這兩種方式都是有效的,應該根據你自己的偏好和代碼風格來選擇
十六、啟動流
很容易使用流來表示來自某個數據源的異步事件。在這種情況下,我們需要一個模擬的 addEventListener 函數,該函數將一段代碼注冊為對傳入事件的響應,并繼續進一步工作。onEach 運算符可以擔任此角色。然而,onEach 是一個中間運算符。我們還需要一個終端運算符來收集數據。否則,只注冊 onEach 是沒有效果的
如果在 onEach 之后使用 collect 終端運算符,則在 collect 之后的代碼將等待流被收集完成后再運行:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
//sampleStart
// 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")
}
//sampleEnd
如你所見,將打印
Event: 1
Event: 2
Event: 3
Done
launchIn 終端運算符在這里是很實用的。通過將 collect 替換為 launchIn,我們可以在單獨的協程中啟動收集流數據的操作,以便立即繼續執行下一步的代碼:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// Imitate a flow of events
fun events(): Flow<Int> = (1..3).asFlow().onEach { delay(100) }
//sampleStart
fun main() = runBlocking<Unit> {
events()
.onEach { event -> println("Event: $event") }
.launchIn(this) // <--- Launching the flow in a separate coroutine
println("Done")
}
//sampleEnd
運行結果:
Done
Event: 1
Event: 2
Event: 3
launchIn 所需的參數用于指定啟動用于收集流的協程的作用域。在上面的示例中,此作用域來自 runBlocking,因此當流運行時,runBlocking 作用域等待其子協程完成,并阻止主函數返回和終止此示例代碼
在實際應用程序中,作用域將來自生命周期是有限的實體。一旦此實體的生命周期終止,相應的作用域將被取消,從而取消相應流的收集。onEach { ... }.launchIn(scope) 的工作方式與 addEventListener 類似。但是,不需要相應的 removeEventListener 函數,因為 cancellation 和結構化并發可以達到這個目的
請注意,launchIn 還返回一個 Job 對象,該 Job 僅可用于取消相應的流數據收集協程,而不取消整個作用域或加入它
十七、Flow and Reactive Streams
For those who are familiar with Reactive Streams or reactive frameworks such as RxJava and project Reactor, design of the Flow may look very familiar.
Indeed, its design was inspired by Reactive Streams and its various implementations. But Flow main goal is to have as simple design as possible, be Kotlin and suspension friendly and respect structured concurrency. Achieving this goal would be impossible without reactive pioneers and their tremendous work. You can read the complete story in Reactive Streams and Kotlin Flows article.
While being different, conceptually, Flow is a reactive stream and it is possible to convert it to the reactive (spec and TCK compliant) Publisher and vice versa. Such converters are provided by kotlinx.coroutines out-of-the-box and can be found in corresponding reactive modules (kotlinx-coroutines-reactive for Reactive Streams, kotlinx-coroutines-reactor for Project Reactor and kotlinx-coroutines-rx2 for RxJava2). Integration modules include conversions from and to Flow, integration with Reactor's Context and suspension-friendly ways to work with various reactive entities.