前言
本文是閱讀協程Flow的總結筆記。
什么是Flow
Kotlin中的Flow API是可以更好的異步處理按順序執行的數據流的方法。
在RxJava中,Observables類型是表示項目流結構的示例。 在訂閱者進行訂閱之前,其主體不會被執行。 訂閱后,訂閱者便開始獲取發射的數據項。 同樣,Flow在相同的條件下工作,即在流生成器內部的代碼到了收集流后才開始運行。
你可以認為他是Kotlin的RxJava,但比RxJava學習成本低,天然與協程友好(協程庫的一部分)
怎樣使用
首先引入kotlin的協程的核心庫
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
編寫如下代碼
fun main() =
runBlocking {
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}
sample().collect {
println("接收到的消息${it}")
}
}
fun sample() = flow<Int> {
repeat(3) {
delay(100)
emit(it)
}
}
運行如下
I'm not blocked 1
接收到的消息0
I'm not blocked 2
接收到的消息1
I'm not blocked 3
接收到的消息2
Process finished with exit code 0
這就是Flow的基礎用法,需要注意的點如下:
(1)flow是Flow的構造方法,Flow使用emit發射數據,使用collect來接收數據(與RxJava的上下游概念類似)
(2)flow代碼塊是可以掛起的
(3)sample函數沒有用suspend關鍵字進行標識
流是冷的
Flow是冷數據流,表現為構建Flow的代碼中的emit需要在調用collect才開始發送數據。
suspend fun main() = coroutineScope {
println("收集數據")
sample1().collect {
println(it)
}
println("再次收集數據")
sample1().collect {
println(it)
}
}
fun sample1() = flow {
repeat(3) {
emit(it)
}
}
代碼運行如下
收集數據
0
1
2
再次收集數據
0
1
2
Process finished with exit code 0
從運行結果可以看到,調用了collect,flow的emit才會調用。
流的超時取消
Flow提供了withTimeoutOrNull來在超時的情況下取消并停止執行其代碼的。
代碼如下:
fun main() = runBlocking {
withTimeoutOrNull(300) {
sample2().collect {
println(it)
}
}
println("完成")
}
fun sample2() = flow {
repeat(3) {
kotlinx.coroutines.delay(100)
emit(it)
}
}
運行結果如下:
0
1
完成
Process finished with exit code 0
可以看到3沒有打印出來
流構造器
Flow的構造器除了flow{}這種還有:
1.asFlow ,擴展函數,將相關的集合或者序列轉換為流
2.flowOf(...),定義固定發射的數據流
代碼如下:
suspend fun main() = coroutineScope {
listOf(1, 2, 3).asFlow().collect {
println(it)
}
flowOf(4, 5, 6).collect {
println(it)
}
}
運行結果如下:
1
2
3
4
5
6
Process finished with exit code 0
過度操作符
Flow中的過度操作符有Map和Filter兩種,其中Map是把Flow中的數據轉換為另外一種數據類型發射出來。Filter則是將符合條件的數據發送出來。
Map代碼如下:
suspend fun main()= coroutineScope {
listOf(1, 2, 3).asFlow().map { it ->
waitAWhile(it)
}.collect {
println(it)
}
}
suspend fun waitAWhile(int: Int): String {
delay(100)
return "等了一會$int"
}
代碼將Int類型轉換為String類型的數據發送出來
運行結果如下:
等了一會1
等了一會2
等了一會3
Fliter操作符代碼如下:
suspend fun main()= coroutineScope {
listOf(1, 2, 3).asFlow().filter {
it>1
}.collect {
println(it)
}
}
代碼中只發射大于1的數據,也就是2,3
運行結果如下:
2
3
轉換操作符
Flow中的轉換操作符主要是TransForm,可以實現更復雜的變換(時間上map和filter都是基于Transform實現的),代碼如下:
listOf(1, 2, 3).asFlow().transform {
emit("${it}發射")
emit("${it}再發射")
}.collect {
println(it)
}
代碼中將發射一次轉換為發射兩次,運行代碼如下:
1發射
1再發射
2發射
2再發射
3發射
3再發射
Process finished with exit code 0
限長操作符
Flow中的限長操作符為Take,在流觸及相應限制的時候會將它的執行取消(與協程一致都是通過拋出異常的方式進行取消,該操作符已經處理了異常拋出)。
代碼如下:
listOf(1, 2, 3).asFlow().take(2).collect {
println(it)
}
代碼中設置了限長為2,標識只會接受兩個發射的數據
運行結果如下:
1
2
末端操作符
末端操作符是在流上用于啟動流收集的掛起函數。 collect 是最基礎的末端操作符,但是還有另外一些更方便使用的末端操作符
toList代碼如下:
suspend fun main() {
val stringList = mutableListOf<String>()
//轉換為list
flowOf(1,2,3).map {
"haha$it"
}.toList(stringList).forEach {
println(it)
}
}
運行結果如下:
haha1
haha2
haha3
Process finished with exit code 0
toSet代碼如下:
flowOf(1, 2, 3).map {
"haha$it"
}.toSet(mutableSetOf()).forEach {
println(it)
}
運行結果如下:
haha1
haha2
haha3
Process finished with exit code 0
toCollection代碼如下:
//轉換為collection
flowOf(1, 2, 3).map {
"haha$it"
}.toCollection(mutableSetOf()).forEach {
println(it)
}
運行結果如下:
haha1
haha2
haha3
Process finished with exit code 0
first:獲取第一個發射的數據,其余的拋棄
代碼如下:
val first = flowOf(1, 2, 3).map {
"haha$it"
}.first()
println(first)
運行結果如下:
haha1
Process finished with exit code 0
first可以通過lambda函數作為選擇條件,返回滿足條件的第一個值,代碼如下
//first 返回第一個滿足條件的元素
val first = flowOf(1, 2, 3).map {
"haha$it"
}.first {
it.contains("2")
}
println(first)
運行結果如下:
haha2
Process finished with exit code 0
single也是返回一個發送的數據,但,他要求至多有一個數據發送,發送多個數據會報錯
代碼如下:
//single返回單個值
val single = flowOf(1).map {
"haha$it"
}.single()
println(single)
運行結果如下:
haha1
Process finished with exit code 0
singleOrNull 單個發射返回數值,否則返回null
//singleOrNull 單個發射返回數值,否則返回null
val singleOrNull = mutableListOf<Int>().asFlow().map {
"haha$it"
}.singleOrNull()
println(singleOrNull)
運行結果如下:
null
Process finished with exit code 0
reduce 對集合進行計算操作
val reduce = flowOf(1, 2, 3)
.reduce { accumulator, value ->
accumulator + value
}
println(reduce)
運行結果如下:
6
Process finished with exit code 0
fold帶初始值的reduce
//帶初始值的reduce
val fold = flowOf(1, 2, 3)
.fold(100) { acc, value ->
acc + value
}
println(fold)
運行結果如下
106
Process finished with exit code 0
流是連續的
流的每次單獨收集都是按順序執行的,除非進行特殊操作的操作符使用多個流(Zip操作符)。該收集過程直接在協程中運行,該協程調用末端操作符。 默認情況下不啟動新協程。 從上游到下游每個過渡操作符都會處理每個發射出的值然后再交給末端操作符。
簡單來說就是一個發射數據執行完所有的操作符(collect除外),下一個數據才能發射并執行相關的操作符
代碼如下:
(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
流的上下文切換
流的收集總是在調用協程的上下文中發生。如無特殊指定,則默認與所在協程處于同一個上下文中。
suspend fun main() {
println(Thread.currentThread().name)
flow {
println(Thread.currentThread().name)
emit(1)
emit(2)
emit(3)
}.collect {
println(Thread.currentThread().name)
println(it)
}
}
運行結果如下
main
main
main
1
main
2
main
3
Process finished with exit code 0
但是如果發射器與接收器不處于同一個上下文時則會報錯
//發射邏輯與接收邏輯不在同一個線程中,會發生報錯
val flow = flow {
withContext(Dispatchers.Default) {
println("發射時的線程:${Thread.currentThread().name}")
emit(1)
emit(2)
emit(3)
}
}
flow.collect {
println(it)
}
運行如下:
···
接收時的線程:main
發射時的線程:DefaultDispatcher-worker-1
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
···
為了實現更高發射器的上下文,可以使用flowOn手動切換所處線程
val flowOn = flow {
println("發射時的線程:${Thread.currentThread().name}")
emit(1)
emit(2)
emit(3)
}.flowOn(Dispatchers.Default)
withContext(Dispatchers.IO) {
flowOn.collect {
println("接收時的線程:${Thread.currentThread().name}")
println(it)
}
}
運行結果如下:
發射時的線程:DefaultDispatcher-worker-3
接收時的線程:DefaultDispatcher-worker-1
1
接收時的線程:DefaultDispatcher-worker-1
2
接收時的線程:DefaultDispatcher-worker-1
3
Process finished with exit code 0
緩沖操作符
從收集流所花費的時間來看,將流的不同部分運行在不同的協程中將會很有幫助,特別是當涉及到長時間運行的異步操作時。例如,考慮一種情況, 一個 發射器 流的發射很慢,它每花費 100 毫秒才產生一個元素;而收集器也非常慢, 需要花費 200 毫秒來處理元素。讓我們看看從該流收集三個數字要花費多長時間。
val measureTimeMillis = measureTimeMillis {
flow {
repeat(3) {
delay(100)
emit(it)
}
}.collect {
delay(200)
println(it)
}
}
println(measureTimeMillis)
運行結果如下:
0
1
2
1031
Process finished with exit code 0
那么如何加快該過程呢,答案是使用Buffer操作符
val measureTimeMillis = measureTimeMillis {
flow {
repeat(3) {
delay(100)
emit(it)
}
}.buffer().collect {
delay(200)
println(it)
}
}
println(measureTimeMillis)
運行結果如下:
0
1
2
985
它產生了相同的數字,只是更快了,由于我們高效地創建了處理流水線, 僅僅需要等待第一個數字產生的 100 毫秒以及處理每個數字各需花費的 200 毫秒。(PS:上邊的flowOn也用到了類似的緩存機制,所以不需要再次調用buffer操作符)。
合并操作符
當流代表部分操作結果或操作狀態更新時,可能沒有必要處理每個值,而是只處理最新的那個。在本示例中,當收集器處理它們太慢的時候, conflate 操作符可以用于跳過中間值。構建前面的示例:
suspend fun main() {
flow {
repeat(3) {
delay(100)
emit(it)
}
}.conflate().collect {
delay(500)
println(it)
}
}
運行結果如下:
0
2
Process finished with exit code 0
沒有打印中間的1
當發射器和收集器都很慢的時候,合并是加快處理速度的一種方式。它通過刪除發射值來實現。 另一種方式是取消緩慢的收集器,并在每次發射新值的時候重新啟動它。有一組與 xxx
操作符執行相同基本邏輯的 xxxLatest
操作符,但是在新值產生的時候取消執行其塊中的代碼。讓我們在先前的示例中嘗試更換 conflate 為 collectLatest:
suspend fun main() {
flow {
repeat(3) {
kotlinx.coroutines.delay(100)
emit(it)
}
}.collectLatest {
println(it)
delay(500)
println(it)
}
}
運行結果如下
2
Process finished with exit code 0
只打印出最后的2,沒有執行0,1的打印
組合操作符
就像 Kotlin 標準庫中的 Sequence.zip 擴展函數一樣, 流擁有一個 zip 操作符用于組合兩個流中的相關值(多余數據直接丟棄)
/**
* zip 組合兩個流中的相關值,多余的進行丟棄
*/
suspend fun main() {
val flow1 = flowOf(1, 2, 3, 4)
val flow2 = flowOf("one", "two", "three")
flow1.zip(flow2) { i: Int, s: String ->
"$s:$i"
}.collect {
println(it)
}
}
運行結果如下
one:1
two:2
three:3
Process finished with exit code 0
當流表示一個變量或操作的最新值時(請參閱相關小節 conflation),可能需要執行計算,這依賴于相應流的最新值,并且每當上游流產生值的時候都需要重新計算。這種相應的操作符家族稱為 combine。
suspend fun main() {
val flow1 = flowOf(1, 2, 3, 4).onEach { delay(300) }
val flow2 = flowOf("one", "two", "three").onEach { delay(400) }
flow1.combine(flow2) { i: Int, s: String ->
"$s:$i"
}.collect {
println(it)
}
}
運行結果如下:
one:1
one:2
two:2
two:3
three:3
three:4
Process finished with exit code 0
展開操作符
流表示異步接收的值序列,所以很容易遇到這樣的情況: 每個值都會觸發對另一個值序列的請求。比如說,我們可以擁有下面這樣一個返回間隔 500 毫秒的兩個字符串流的函數:
fun reqeustFlow(it: Int) = flow {
emit("${it}:first")
kotlinx.coroutines.delay(500)
emit("${it}:second")
}
現在,如果我們有一個包含三個整數的流,并為每個整數調用 requestFlow
val map = flowOf(1, 2, 3).map {
reqeustFlow(it)
}
然后我們得到了一個包含流的流(Flow<Flow<String>>
),需要將其進行展平為單個流以進行下一步處理。集合與序列都擁有 flatten 與 flatMap 操作符來做這件事。然而,由于流具有異步的性質,因此需要不同的展平模式。
flatMapConcat
順序的將發射器發射的數據轉變為一個新的流
flowOf(1, 2, 3).onEach { delay(100) }.flatMapConcat {
reqeustFlow(it)
}.collect {
println(it)
}
運行結果如下:
1:first
1:second
2:first
2:second
3:first
3:second
Process finished with exit code 0
flatMapMerge
并發收集所有傳入的流,并將它們的值合并到一個單獨的流,以便盡快的發射值。需要注意的是flatMapMerge 會順序調用代碼塊(本示例中的 { requestFlow(it) }
),但是并發收集結果流,相當于執行順序是首先執行 map { requestFlow(it) }
然后在其返回結果上調用 flattenMerge。從現象上來說就是:先獲取所有的發射項,然后每個發射項逐次調用flatMapMerge中的代碼(flatMapMerge有a,b代碼,所有發射項先執行a,執行完后所有發射項再執行b)。
flowOf(1, 2, 3).onEach { delay(100) }.flatMapMerge {
reqeustFlow(it)
}.collect {
println(it)
}
運行代碼如下:
1:first
2:first
3:first
1:second
2:second
3:second
Process finished with exit code 0
flatMapLatest
在發出新流后立即取消先前流的收集
flowOf(1, 2, 3).onEach { delay(100) }.flatMapLatest {
reqeustFlow(it)
}.collect {
println(it)
}
運行結果如下:
1:first
2:first
3:first
3:second
Process finished with exit code 0
try-catch
flow在收集器代碼塊使用try-catch的話,會接收到flow所有的異常拋出。我們首先看發射器發生錯誤的時候
val fl = flow {
emit(1)
emit(2)
emit(3)
}.map {
check(it <3) {
"上游發生錯誤"
}
it
}
try {
fl.collect {
println(it)
}
} catch (e: Exception) {
println(e.message)
}
運行結果如下:
1
2
上游發生錯誤
Process finished with exit code 0
這里需要注意的是check操作符,如果不符合條件才會執行大括號內的代碼,并將返回值作為異常的消息。
如果下游發生異常的話,是否能捕獲到異常呢?,看如下代碼
val flowOf = flowOf(1, 2, 3)
try {
flowOf.collect {
check(it <= 2) {
"下游發生錯誤"
}
println(it)
}
} catch (e: Exception) {
println(e.message)
}
運行結果如下:
1
2
下游發生錯誤
Process finished with exit code 0
可以看到如果用try-catch包圍收集器的話是可以捕獲到上下游的異常的,需要注意的是流必須對異常透明,即在 flow { ... }
構建器內部的 try/catch
塊中發射值是違反異常透明性的。這樣可以保證收集器拋出的一個異常能被像先前示例中那樣的 try/catch
塊捕獲。那么Flow有沒有專門的捕獲異常的操作符呢,答案是有的,他就是catch操作符。
flowOf(1, 2, 3).map {
check(it <= 2) {
"上游出現錯誤"
}
it
}.catch {
println(it.message)
}.collect {
println(it)
}
運行結果如下:
1
2
上游出現錯誤
Process finished with exit code 0
但是這樣是不能獲取到接收器發生的錯誤的。如果想捕獲發射器收集器的異常,可采取如下的方法
flowOf(1, 2, 3).map {
check(it < 3) {
"上游發生了錯誤"
}
it
}.onEach {
//放開該注釋,注釋掉上面的check代碼,則能捕獲下游發生的錯誤
// check(it < 2) {
// "下游發生錯誤"
// }
println(it)
}.catch {
println(it.message)
}.collect()
運行結果如下:
1
2
上游發生了錯誤
Process finished with exit code 0
至于你采用try-catch還是catch操作符,那就取決于你的習慣了。
流完成
流的完成也有finally和onCompletion操作符兩種。先看finally這種吧
val map = flowOf(1, 2, 3).map {
check(it < 3) {
"上游發生錯誤"
}
it
}
try {
map.collect {
println(it)
}
} catch (e: Exception) {
println(e.message)
}finally {
println("結束了")
}
運行結果如下
1
2
上游發生錯誤
結束了
Process finished with exit code 0
onCompletion操作符如下:
flowOf(1, 2, 3).map {
check(it <= 2) {
"上游發生錯誤"
}
it
}.catch {
println(it.message)
}.onCompletion {
if (it != null) {
println("發生了異常")
} else {
println("沒有異常")
}
}.collect {
println(it)
}
運行結果
1
2
上游發生錯誤
沒有異常
Process finished with exit code 0
這里需要注意的是onComplettion是可以捕獲到相關異常的,但他不能處理異常,異常仍會向下游流動。本文中的代碼因為在onCompletion之前調用了catch,捕獲了異常,所以
onCompletion不會收到相關異常信息。
啟動流
使用流表示來自一些源的異步事件是很簡單的。 在這個案例中,我們需要一個類似 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 末端操作符可以在這里派上用場。使用 launchIn
替換 collect
我們可以在單獨的協程中啟動流的收集,這樣就可以立即繼續進一步執行代碼:
fun 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
函數, 因為取消與結構化并發可以達成這個目的。