Kotlin 協程三 —— 數據流 Flow

Kotlin 協程系列文章導航:
Kotlin 協程一 —— 協程 Coroutine
Kotlin 協程二 —— 通道 Channel
Kotlin 協程三 —— 數據流 Flow
Kotlin 協程四 —— Flow 和 Channel 的應用
Kotlin 協程五 —— 在Android 中使用 Kotlin 協程

一、Flow 的基本使用

Kotlin 協程中使用掛起函數可以實現非阻塞地執行任務並將結果返回回來,但是隻能返回單個計算結果。但是如果希望有多個計算結果返回回來,則可以使用 Flow。

1.1 Sequence 與 Flow

介紹 Flow 之前,先看下序列生成器:

val intSequence = sequence<Int> {
        Thread.sleep(1000) // 模擬耗時任務1
        yield(1)
        Thread.sleep(1000) // 模擬耗時任務2
        yield(2)
        Thread.sleep(1000) // 模擬耗時任務3
        yield(3)
    }

intSequence.forEach {
        println(it)
    }

如上,取出序列生成器中的值,需要迭代序列生成器,按照我們的預期,依次返回了三個結果。

Sequence 是同步調用,是阻塞的,無法調用其它的掛起函數。顯然,我們更多地時候希望能夠異步執行多個任務,並將結果依次返回回來,Flow 是該場景的最優解。

Flow 源碼如下,只有一個 collect 方法。

public interface Flow<out T> {

    @InternalCoroutinesApi
    public suspend fun collect(collector: FlowCollector<T>)
}

Flow 可以非阻塞地執行多個任務,並返回多個結果, 在 Flow 中可以調用其它掛起函數。取出 Flow 中的值,則需要調用 collect 方法,Flow 的使用形式爲:

Flow.collect()  // 僞代碼

由於 collect 是一個掛起函數, collect 方法必須要在協程中調用。

1.2 Flow 的簡單使用

實現上述 Sequence 類似的效果:

private fun createFlow(): Flow<Int> = flow {
    delay(1000)
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

fun main() = runBlocking {
    createFlow().collect {
        println(it)
    }
}

上述代碼使用 flow{ ... } 來構建一個 Flow 類型,具有如下特點:

  • flow{ ... } 內部可以調用 suspend 函數;
  • createFlow 不需要 suspend 來標記;(爲什麼沒有標記爲掛起函數,去可以調用掛起函數?)
  • 使用 emit() 方法來發射數據;
  • 使用 collect() 方法來收集結果。

1.3 創建常規 Flow 的常用方式:

flow{...}

flow {
    delay(1000)
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

flowOf()

flowOf(1,2,3).onEach {
    delay(1000)
}

flowOf() 構建器定義了一個發射固定值集的流, 使用 flowOf 構建 Flow 不需要顯示調用 emit() 發射數據

asFlow()

listOf(1, 2, 3).asFlow().onEach {
    delay(1000)
}

使用 asFlow() 擴展函數,可以將各種集合與序列轉換爲流,也不需要顯示調用 emit() 發射數據

1.4 Flow 是冷流(惰性的)

如同 Sequences 一樣, Flows 也是惰性的,即在調用末端流操作符(collect 是其中之一)之前, flow{ ... } 中的代碼不會執行。我們稱之爲冷流。

private fun createFlow(): Flow<Int> = flow {
    println("flow started")
    delay(1000)
    emit(1)
    delay(1000)
    emit(2)
    delay(1000)
    emit(3)
}

fun main() = runBlocking {
    val flow = createFlow()
    println("calling collect...")
    flow.collect {
        println(it)
    }
    println("calling collect again...")
    flow.collect {
        println(it)
    }
}

結果如下:

calling collect...
flow started
1
2
3
calling collect again...
flow started
1
2
3

這是一個返回一個 Flow 的函數 createFlow 沒有標記 suspend 的原因,即便它內部調用了 suspend 函數,但是調用 createFlow 會立即返回,且不會進行任何等待。而再每次收集結果的時候,纔會啓動流。

那麼有沒有熱流呢? 後面講的 ChannelFlow 就是熱流。只有上游產生了數據,就會立即發射給下游的收集者。

1.5 Flow 的取消

流採用了與協程同樣的協助取消。流的收集可以在當流在一個可取消的掛起函數(例如 delay)中掛起的時候取消。取消Flow 只需要取消它所在的協程即可。
以下示例展示了當 withTimeoutOrNull 塊中代碼在運行的時候流是如何在超時的情況下取消並停止執行其代碼的:

fun simple(): Flow<Int> = flow { 
    for (i in 1..3) {
        delay(100)          
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    withTimeoutOrNull(250) { // 在 250 毫秒後超時
        simple().collect { value -> println(value) } 
    }
    println("Done")
}

注意,在 simple 函數中流僅發射兩個數字,產生以下輸出:

Emitting 1
1
Emitting 2
2
Done

二、 Flow 的操作符

2.1 Terminal flow operators 末端流操作符

末端操作符是在流上用於啓動流收集的掛起函數。 collect 是最基礎的末端操作符,但是還有另外一些更方便使用的末端操作符:

  • 轉化爲各種集合,toList/toSet/toCollection
  • 獲取第一個(first)值,最後一個(last)值與確保流發射單個(single)值的操作符
  • 使用 reduce 與 fold 將流規約到單個值
  • count
  • launchIn/produceIn/broadcastIn

下面看幾個常用的末端流操作符

2.1.1 collect

收集上游發送的數據

2.1.2 reduce

reduce 類似於 Kotlin 集合中的 reduce 函數,能夠對集合進行計算操作。前面提到,reduce 是一個末端流操作符。

fun main() = runBlocking {
    val sum = (1..5).asFlow().reduce { a, b ->
        a + b
    }
    println(sum)
}

輸出結果:

15

2.1.3 fold

fold 也類似於 Kotlin 集合中的 fold,需要設置一個初始值,fold 也是一個末端流操作符。

fun main() = runBlocking {
    val sum = (1..5).asFlow().fold(100) { a, b ->
        a + b
    }
    println(sum)
}

輸出結果:

115

2.1.4 launchIn

launchIn 用來在指定的 CoroutineScope 內啓動 flow, 需要傳入一個參數: CoroutineScope
源碼:

public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
    collect() // tail-call
}

示例:

private val mDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main() {
    val scope = CoroutineScope(mDispatcher)
    (1..5).asFlow().onEach { println(it) }
        .onCompletion { mDispatcher.close() }
        .launchIn(scope)
}

輸出結果:

1
2
3
4
5

再看一個例子:

fun main() = runBlocking{
    val cosTime = measureTimeMillis {
        (1..5).asFlow()
            .onEach { delay(100) }
            .flowOn(Dispatchers.IO)
            .collect { println(it) }

        flowOf("one", "two", "three", "four", "five")
            .onEach { delay(200) }
            .flowOn(Dispatchers.IO)
            .collect { println(it) }
    }
    println("cosTime: $cosTime")
}

我們希望並行執行兩個 Flow ,看下輸出結果:

1
2
3
4
5
one
two
three
four
five
cosTime: 1645

結果並不是並行執行的,這個很好理解,因爲第一個 collect 不執行完,不會走到第二個。

正確地寫法應該是,爲每個 Flow 單獨起一個協程:

fun main() = runBlocking<Unit>{
    launch {
        (1..5).asFlow()
            .onEach { delay(100) }
            .flowOn(Dispatchers.IO)
            .collect { println(it) }
    }
    launch {
        flowOf("one", "two", "three", "four", "five")
            .onEach { delay(200) }
            .flowOn(Dispatchers.IO)
            .collect { println(it) }
    }
}

或者使用 launchIn, 寫法更優雅:

fun main() = runBlocking<Unit>{

    (1..5).asFlow()
        .onEach { delay(100) }
        .flowOn(Dispatchers.IO)
        .onEach { println(it) }
        .launchIn(this)

    flowOf("one", "two", "three", "four", "five")
        .onEach { delay(200) }
        .flowOn(Dispatchers.IO)
        .onEach { println(it) }
        .launchIn(this)
}

輸出結果:

1
one
2
3
4
two
5
three
four
five

2.2 流是連續的

與 Sequence 類似,Flow 的每次單獨收集都是按順序執行的,除非進行特殊操作的操作符使用多個流。 默認情況下不啓動新協程。 從上游到下游每個過渡操作符都會處理每個發射出的值然後再交給末端操作符。

fun main() = runBlocking {
    (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

2.3 onStart 流啓動時

Flow 啓動開始執行時的回調,在耗時操作時可以用來做 loading。

fun main() = runBlocking {
    (1..5).asFlow()
        .onEach { delay(200) }
        .onStart { println("onStart") }
        .collect { println(it) }
}

輸出結果:

onStart
1
2
3
4
5

2.4 onCompletion 流完成時

Flow 完成時(正常或出現異常時),如果需要執行一個操作,它可以通過兩種方式完成:

2.4.1 使用 try ... finally 實現

fun main() = runBlocking {
    try {
        flow {
            for (i in 1..5) {
                delay(100)
                emit(i)
            }
        }.collect { println(it) }
    } finally {
        println("Done")
    }
}

2.4.2 通過 onCompletion 函數實現

fun main() = runBlocking {
    flow {
        for (i in 1..5) {
            delay(100)
            emit(i)
        }
    }.onCompletion { println("Done") }
        .collect { println(it) }
}

輸出:

1
2
3
4
5
Done

2.5 Backpressure 背壓

Backpressure 是響應式編程的功能之一, Rxjava2 中的 Flowable 支持的 Backpressure 策略有:

  • MISSING:創建的 Flowable 沒有指定背壓策略,不會對通過 OnNext 發射的數據做緩存或丟棄處理。
  • ERROR:如果放入 Flowable 的異步緩存池中的數據超限了,則會拋出 MissingBackpressureException 異常。
  • BUFFER:Flowable 的異步緩存池同 Observable 的一樣,沒有固定大小,可以無限制添加數據,不會拋出 MissingBackpressureException 異常,但會導致 OOM。
  • DROP:如果 Flowable 的異步緩存池滿了,會丟掉將要放入緩存池中的數據。
  • LATEST:如果緩存池滿了,會丟掉將要放入緩存池中的數據。這一點跟 DROP 策略一樣,不同的是,不管緩存池的狀態如何,LATEST 策略會將最後一條數據強行放入緩存池中。

而在Flow代碼塊中,每當有一個處理結果 我們就可以收到,但如果處理結果也是耗時操作。就有可能出現,發送的數據太多了,處理不及時的情況。
Flow 的 Backpressure 是通過 suspend 函數實現的。

2.5.1 buffer 緩衝

buffer 對應 Rxjava 的 BUFFER 策略。 buffer 操作指的是設置緩衝區。當然緩衝區有大小,如果溢出了會有不同的處理策略。

  • 設置緩衝區,如果溢出了,則將當前協程掛起,直到有消費了緩衝區中的數據。
  • 設置緩衝區,如果溢出了,丟棄最新的數據。
  • 設置緩衝區,如果溢出了,丟棄最老的數據。

緩衝區的大小可以設置爲 0,也就是不需要緩衝區。

看一個未設置緩衝區的示例,假設每產生一個數據然後發射出去,要耗時 100ms ,每次處理一個數據需要耗時 700ms,代碼如下:

fun main() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow().onEach {
            delay(100)
            println("produce data: $it")
        }.collect {
            delay(700)
            println("collect: $it")
        }
    }
    println("cosTime: $cosTime")
}

結果如下:

produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
produce data: 4
collect: 4
produce data: 5
collect: 5
cosTime: 4069

由於流是惰性的,且是連續的,所以整個流中的數據處理完成大約需要 4000ms

下面,我們使用 buffer() 設置一個緩衝區。buffer(),
接收兩個參數,第一個參數是 size, 表示緩衝區的大小。第二個參數是 BufferOverflow, 表示緩衝區溢出之後的處理策略,其值爲下面的枚舉類型,默認是 BufferOverflow.SUSPEND

處理策略源碼如下:

public enum class BufferOverflow {
    /**
     * Suspend on buffer overflow.
     */
    SUSPEND,

    /**
     * Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend.
     */
    DROP_OLDEST,

    /**
     * Drop **the latest** value that is being added to the buffer right now on buffer overflow
     * (so that buffer contents stay the same), do not suspend.
     */
    DROP_LATEST
}

設置緩衝區,並採用掛起的策略

修改,代碼,我們設置緩衝區大小爲 1 :

fun main() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow().onEach {
            delay(100)
            println("produce data: $it")
        }.buffer(2, BufferOverflow.SUSPEND)
            .collect {
                delay(700)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")
}

結果如下:

produce data: 1
produce data: 2
produce data: 3
produce data: 4
collect: 1
produce data: 5
collect: 2
collect: 3
collect: 4
collect: 5
cosTime: 3713

可見整體用時大約爲 3713ms。buffer 操作符可以使發射和收集的代碼併發運行,從而提高效率。
下面簡單分析一下執行流程:
這裏要注意的是,buffer 的容量是從 0 開始計算的。
首先,我們收集第一個數據,產生第一個數據,然後 2、3、4 存儲在了緩衝區。第5個數據發射時,緩衝區滿了,會掛起。等到第1個數據收集完成之後,再發射第5個數據。

設置緩衝區,丟棄最新的數據
如果上述代碼處理緩存溢出策略爲 BufferOverflow.DROP_LATEST,代碼如下:

fun main() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow().onEach {
            delay(100)
            println("produce data: $it")
        }.buffer(2, BufferOverflow.DROP_LATEST)
            .collect {
                delay(700)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")
}

輸出如下:

produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 2
collect: 3
cosTime: 2272

可以看到,第4個數據和第5個數據因爲緩衝區滿了直接被丟棄了,不會被收集。

設置緩衝區,丟棄舊的數據
如果上述代碼處理緩存溢出策略爲 BufferOverflow.DROP_OLDEST,代碼如下:

fun main() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow().onEach {
            delay(100)
            println("produce data: $it")
        }.buffer(2, BufferOverflow.DROP_OLDEST)
            .collect {
                delay(700)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")
}

輸出結果如下:

produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 4
collect: 5
cosTime: 2289

可以看到,第4個數據進入緩衝區時,會把第2個數據丟棄掉,第5個數據進入緩衝區時,會把第3個數據丟棄掉。

2.5.2 conflate 合併

當流代表部分操作結果或操作狀態更新時,可能沒有必要處理每個值,而是隻處理最新的那個。conflate 操作符可以用於跳過中間值:

fun main() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow().onEach {
            delay(100)
            println("produce data: $it")
        }.conflate()
            .collect {
                delay(700)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")
}

輸出結果:

produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 5
cosTime: 1596

conflate 操作符是不設緩衝區,也就是緩衝區大小爲 0,丟棄舊數據,也就是採取 DROP_OLDEST 策略,其實等價於 buffer(0, BufferOverflow.DROP_OLDEST) 。

2.6 Flow 異常處理

2.6.1 catch 操作符捕獲上游異常

前面提到的 onCompletion 用來Flow是否收集完成,即使是遇到異常也會執行。

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.onCompletion {
        println("onCompletion")
    }.collect {
        println("collect: $it")
    }
}

輸出:

produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
onCompletion
Exception in thread "main" java.lang.Exception: test exception
...

其實在 onCompletion 中是可以判斷是否有異常的, onCompletion(action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit) 是有一個參數的,如果flow 的上游出現異常,這個參數不爲 null,如果上游未出現異常,則爲 null, 據此,我們可以在 onCompletion 中判斷異常:

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.onCompletion { cause ->
        if (cause != null) {
            println("flow completed exception")
        } else {
            println("onCompletion")
        }
    }.collect {
        println("collect: $it")
    }
}

但是, onCompletion 智能判斷是否出現了異常,並不能捕獲異常。

捕獲異常可以使用 catch 操作符。

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.onCompletion { cause ->
        if (cause != null) {
            println("flow completed exception")
        } else {
            println("onCompletion")
        }
    }.catch { ex ->
        println("catch exception: ${ex.message}")
    }.collect {
        println("collect: $it")
    }
}

輸出結果:

produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
flow completed exception
catch exception: test exception

但是如果把 onCompletioncatch 交換一下位置,則 catch 操作捕獲到異常之後,不會再影響下游:
代碼:

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.catch { ex ->
        println("catch exception: ${ex.message}")
    }.onCompletion { cause ->
        if (cause != null) {
            println("flow completed exception")
        } else {
            println("onCompletion")
        }
    }.collect {
        println("collect: $it")
    }
}

輸出結果:

produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
catch exception: test exception
onCompletion
  • catch 操作符用於實現異常透明化處理, catch 只是中間操作符不能捕獲下游的異常,。
  • catch 操作符內,可以使用 throw 再次拋出異常、可以使用 emit() 轉換爲發射值、可以用於打印或者其他業務邏輯的處理等等

2.6.2 retry、retryWhen 操作符重試

public fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
    require(retries > 0) { "Expected positive amount of retries, but had $retries" }
    return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}

如果上游遇到了異常,並使用了 retry 操作符,則 retry 會讓 Flow 最多重試 retries 指定的次數

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.retry(2) {
        it.message == "test exception"
    }.catch { ex ->
        println("catch exception: ${ex.message}")
    }.collect {
        println("collect: $it")
    }
}

輸出結果:

produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
catch exception: test exception

注意,只有遇到異常了,並且 retry 方法返回 true 的時候纔會進行重試。

retry 最終調用的是 retryWhen 操作符。下面的代碼與上面的代碼邏輯一致。

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        if (it == 4) {
            throw Exception("test exception")
        }
        delay(100)
        println("produce data: $it")
    }.retryWhen { cause, attempt ->
        cause.message == "test exception" && attempt < 2
    }.catch { ex ->
        println("catch exception: ${ex.message}")
    }.collect {
        println("collect: $it")
    }
}

試想: 如果 將代碼中的 catch 和 retry/retryWhen 交換位置,結果如何?

2.7 Flow 線程切換

2.7.1 響應線程

Flow 是基於 CoroutineContext 進行線程切換的。因爲 Collect 是一個 suspend 函數,必須在 CoroutineScope 中執行,所以響應線程是由 CoroutineContext 決定的。比如,在 Main 線程總執行 collect, 那麼響應線程就是 Dispatchers.Main。

2.7.2 flowOn 切換線程

Rxjava 通過 subscribeOn 和 ObserveOn 來決定發射數據和觀察者的線程。並且,上游多次調用 subscribeOn 只會以最後一次爲準。
而 Flows 通過 flowOn 方法來切換線程,多次調用,都會影響到它上游的代碼。舉個例子:

private val mDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

fun main() = runBlocking {
    (1..5).asFlow().onEach {
        printWithThreadInfo("produce data: $it")
    }.flowOn(Dispatchers.IO)
        .map {
            printWithThreadInfo("$it to String")
            "String: $it"
        }.flowOn(mDispatcher)
        .onCompletion {
            mDispatcher.close()
        }
        .collect {
            printWithThreadInfo("collect: $it")
        }
}

輸出結果如下:

thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 1
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 2
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 3
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 4
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 5
thread id: 12, thread name: pool-1-thread-1 ---> 1 to String
thread id: 12, thread name: pool-1-thread-1 ---> 2 to String
thread id: 1, thread name: main ---> collect: String: 1
thread id: 12, thread name: pool-1-thread-1 ---> 3 to String
thread id: 1, thread name: main ---> collect: String: 2
thread id: 12, thread name: pool-1-thread-1 ---> 4 to String
thread id: 1, thread name: main ---> collect: String: 3
thread id: 12, thread name: pool-1-thread-1 ---> 5 to String
thread id: 1, thread name: main ---> collect: String: 4
thread id: 1, thread name: main ---> collect: String: 5

可以看到,發射數據是在 Dispatchers.IO 線程執行的, map 操作時在我們自定義的線程池中進行的,collect 操作在 Dispatchers.Main 線程進行。

2.8 Flow 中間轉換操作符

2.8.1 map

前面例子已經有用到 map 操作符,map 操作符不止可以用於 Flow, map 操作符勇於 List 表示將 List 中的每個元素轉換成新的元素,並添加到一個新的 List 中,最後再講新的 List 返回,
map 操作符用於 Flow 表示將流中的每個元素進行轉換後再發射出來。

fun main() = runBlocking {
    (1..5).asFlow().map { "string: $it" }
        .collect {
            println(it)
        }
}

輸出:

string: 1
string: 2
string: 3
string: 4
string: 5

2.8.2 transform

在使用 transform 操作符時,可以任意多次調用 emit ,這是 transform 跟 map 最大的區別:

fun main() = runBlocking {
    (1..5).asFlow().transform {
        emit(it * 2)
        delay(100)
        emit("String: $it")
    }.collect {
            println(it)
        }
}

輸出結果:

2
String: 1
4
String: 2
6
String: 3
8
String: 4
10
String: 5

2.8.3 onEach

遍歷

fun main() = runBlocking {
    (1..5).asFlow()
        .onEach { println("onEach: $it") }
        .collect { println(it) }
}

輸出:

onEach: 1
1
onEach: 2
2
onEach: 3
3
onEach: 4
4
onEach: 5
5

2.8.4 filter

按條件過濾

fun main() = runBlocking {
    (1..5).asFlow()
        .filter { it % 2 == 0 }
        .collect { println(it) }
}

輸出結果:

2
4

2.8.5 drop / dropWhile

drop 過濾掉前幾個元素
dropWhile 過濾滿足條件的元素

2.8.6 take

take 操作符只取前幾個 emit 發射的值

fun main() = runBlocking {
    (1..5).asFlow().take(2).collect {
            println(it)
        }
}

輸出:

1
2

2.8.7 zip

zip 是可以將2個 flow 進行合併的操作符

fun main() = runBlocking {
    val flowA = (1..6).asFlow()
    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200)
    flowA.zip(flowB) { a, b -> "$a and $b" }
        .collect {
            println(it)
        }
}

輸出結果:

1 and one
2 and two
3 and three
4 and four
5 and five

zip 操作符會把 flowA 中的一個 item 和 flowB 中對應的一個 item 進行合併。即使 flowB 中的每一個 item 都使用了 delay() 函數,在合併過程中也會等待 delay() 執行完後再進行合併。

如果 flowA 和 flowB 中 item 個數不一致,則合併後新的 flow item 個數,等於較小的 item 個數

fun main() = runBlocking {
    val flowA = (1..5).asFlow()
    val flowB = flowOf("one", "two", "three","four","five", "six", "seven").onEach { delay(200) }
    flowA.zip(flowB) { a, b -> "$a and $b" }
        .collect {
            println(it)
        }
}

輸出結果:

1 and one
2 and two
3 and three
4 and four
5 and five

2.8.8 combine

combine 也是合併,但是跟 zip 不太一樣。

使用 combine 合併時,每次從 flowA 發出新的 item ,會將其與 flowB 的最新的 item 合併。

fun main() = runBlocking {
    val flowA = (1..5).asFlow().onEach { delay(100) }
    val flowB = flowOf("one", "two", "three","four","five", "six", "seven").onEach { delay(200) }
    flowA.combine(flowB) { a, b -> "$a and $b" }
        .collect {
            println(it)
        }
}

輸出結果:

1 and one
2 and one
3 and one
3 and two
4 and two
5 and two
5 and three
5 and four
5 and five
5 and six
5 and seven

2.8.9 flattenContact 和 flattenMerge 扁平化處理

flattenContact
flattenConcat 將給定流按順序展平爲單個流,而不交錯嵌套流。
源碼:

@FlowPreview
public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
    collect { value -> emitAll(value) }
}

例子:

fun main() = runBlocking {
    val flowA = (1..5).asFlow()
    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(1000) }

    flowOf(flowA,flowB)
        .flattenConcat()
        .collect{ println(it) }
}

輸出:

1
2
3
4
5
// delay 1000ms
one
// delay 1000ms
two
// delay 1000ms
three
// delay 1000ms
four
// delay 1000ms
five

flattenMerge
fattenMerge 有一個參數,併發限制,默認位 16。
源碼:

@FlowPreview
public fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> {
    require(concurrency > 0) { "Expected positive concurrency level, but had $concurrency" }
    return if (concurrency == 1) flattenConcat() else ChannelFlowMerge(this, concurrency)
}

可見,參數必須大於0, 並且參數爲 1 時,與 flattenConcat 一致。

fun main() = runBlocking {
    val flowA = (1..5).asFlow().onEach { delay(100) }
    val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }

    flowOf(flowA,flowB)
        .flattenMerge(2)
        .collect{ println(it) }
}

輸出結果:

1
one
2
3
two
4
5
three
four
five

2.8.10 flatMapMerge 和 flatMapContact

flatMapMerge 由 map、flattenMerge 操作符實現

@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(
    concurrency: Int = DEFAULT_CONCURRENCY,
    transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)

例子:

fun main() = runBlocking {
    (1..5).asFlow()
        .flatMapMerge {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }.collect { println(it) }
}

輸出結果:

1
2
3
4
5
// delay 1000ms
string: 1
string: 2
string: 3
string: 4
string: 5

flatMapContact 由 map、flattenConcat 操作符實現

@FlowPreview
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
    map(transform).flattenConcat()

例子:

fun main() = runBlocking {
    (1..5).asFlow()
        .flatMapConcat {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }.collect { println(it) }
}

輸出結果:

1
// delay 1000ms
string: 1
2
// delay 1000ms
string: 2
3
// delay 1000ms
string: 3
4
// delay 1000ms
string: 4
5
// delay 1000ms
string: 5

flatMapMerge 和 flatMapContact 都是將一個 flow 轉換成另一個流。
區別在於: flatMapMerge 不會等待內部的 flow 完成 , 而調用 flatMapConcat 後,collect 函數在收集新值之前會等待 flatMapConcat 內部的 flow 完成。

2.8.11 flatMapLatest

當發射了新值之後,上個 flow 就會被取消。

fun main() = runBlocking {
    (1..5).asFlow().onEach { delay(100) }
        .flatMapLatest {
            flow {
                println("begin flatMapLatest $it")
                delay(200)
                emit("string: $it")
                println("end flatMapLatest $it")
            }
        }.collect {
            println(it)
        }
}

輸出結果:

begin flatMapLatest 1
begin flatMapLatest 2
begin flatMapLatest 3
begin flatMapLatest 4
begin flatMapLatest 5
end flatMapLatest 5
string: 5

三、 StateFlow 和 SharedFlow

StateFlow 和 SharedFlow 是用來替代 BroadcastChannel 的新的 API。用於上游發射數據,能同時被多個訂閱者收集數據。

3.1 StateFlow

官方文檔解釋:StateFlow 是一個狀態容器式可觀察數據流,可以向其收集器發出當前狀態更新和新狀態更新。還可通過其 value 屬性讀取當前狀態值。如需更新狀態並將其發送到數據流,請爲 MutableStateFlow 類的 value 屬性分配一個新值。

在 Android 中,StateFlow 非常適合需要讓可變狀態保持可觀察的類。

StateFlow有兩種類型: StateFlow 和 MutableStateFlow :

public interface StateFlow<out T> : SharedFlow<T> {
   public val value: T
}

public interface MutableStateFlow<out T>: StateFlow<T>, MutableSharedFlow<T> {
   public override var value: T
   public fun compareAndSet(expect: T, update: T): Boolean
}

狀態由其值表示。任何對值的更新都會反饋新值到所有流的接收器中。

3.1.1 StateFlow 基本使用

使用示例:

class Test {
    private val _state = MutableStateFlow<String>("unKnown")
    val state: StateFlow<String> get() = _state

    fun getApi(scope: CoroutineScope) {
        scope.launch {
            val res = getApi()
            _state.value = res
        }
    }

    private suspend fun getApi() = withContext(Dispatchers.IO) {
        delay(2000) // 模擬耗時請求
        "hello, stateFlow"
    }
}

fun main() = runBlocking<Unit> {
    val test: Test = Test()

    test.getApi(this) // 開始獲取結果

    launch(Dispatchers.IO) {
        test.state.collect {
            printWithThreadInfo(it)
        }
    }
    launch(Dispatchers.IO) {
        test.state.collect {
            printWithThreadInfo(it)
        }
    }
}

結果輸出如下,並且程序是沒有停下來的。

thread id: 14, thread name: DefaultDispatcher-worker-3 ---> unKnown
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> unKnown
// 等待兩秒
thread id: 14, thread name: DefaultDispatcher-worker-3 ---> hello, stateFlow
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> hello, stateFlow

StateFlow 的使用方式與 LiveData 類似。
MutableStateFlow 是可變類型的,即可以改變 value 的值。 StateFlow 則是隻讀的。這與 LiveData、MutableLiveData一樣。爲了程序的封裝性。一般對外暴露不可變的只讀變量。

輸出結果證明:

  1. StateFlow 是發射的數據可以被在不同的協程中,被多個接受者同時收集的。
  2. StateFlow 是熱流,只要數據發生變化,就會發射數據。

程序沒有停下來,因爲在 StateFlow 的收集者調用 collect 會掛起當前協程,而且永遠不會結束。

StateFlow 與 LiveData 的不同之處:

  1. StateFlow 必須有初始值,LiveData 不需要。
  2. LiveData 會與 Activity 聲明週期綁定,當 View 進入 STOPED 狀態時, LiveData.observer() 會自動取消註冊,而從 StateFlow 或任意其他數據流收集數據的操作並不會停止。

3.1.2 爲什麼使用 StateFlow

我們知道 LiveData 有如下特點:

  1. 只能在主線程更新數據,即使在子線程通過 postValue()方法,最終也是將值 post 到主線程調用的 setValue()
  2. LiveData 是不防抖的
  3. LiveData 的 transformation 是在主線程工作
  4. LiveData 需要正確處理 “粘性事件” 問題。

鑑於此,使用 StateFlow 可以輕鬆解決上述場景。

3.1.3 防止任務泄漏

解決辦法有兩種:

  1. 不直接使用 StateFlow 的收集者,使用 asLiveData() 方法將其轉換爲 LiveData 使用。————爲何不直接使用 LiveData, 有毛病?
  2. 手動取消 StateFlow 的訂閱者的協程,在 Android 中,可以從 Lifecycle.repeatOnLifecycle 塊收集數據流。

對應代碼如下:

lifecycleSope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        test.state.collect {
            printWithThreadInfo(it)
        }
    }
}

3.1.4 SateFlow 只會發射最新的數據給訂閱者。

我們修改上面代碼:

class Test {
    private val _state = MutableStateFlow<String>("unKnown")
    val state: StateFlow<String> get() = _state

    fun getApi1(scope: CoroutineScope) {
        scope.launch {
            delay(2000)
            _state.value = "hello,coroutine"
        }
    }

    fun getApi2(scope: CoroutineScope) {
        scope.launch {
            delay(2000)
            _state.value = "hello, kotlin"
        }
    }
}

fun main() = runBlocking<Unit> {
    val test: Test = Test()

    test.getApi1(this) // 開始獲取結果
    delay(1000)
    test.getApi2(this) // 開始獲取結果

    val job1 = launch(Dispatchers.IO) {
        delay(8000)
        test.state.collect {
            printWithThreadInfo(it)
        }
    }
    val job2 = launch(Dispatchers.IO) {
        delay(8000)
        test.state.collect {
            printWithThreadInfo(it)
        }
    }

    // 避免任務泄漏,手動取消
    delay(10000)
    job1.cancel()
    job2.cancel()
}

現在的場景是,先請求 getApi1(), 一秒之後再次請求 getApi2(), 這樣 stateFlow 的值加上初始值,一共被賦值過 3 次。確保,三次賦值都完成後,我們再收集 StateFlow 中的數據。
輸出結果如下:

thread id: 13, thread name: DefaultDispatcher-worker-2 ---> hello, kotlin
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> hello, kotlin

結果顯示了,StateFlow 只會將最新的數據發射給訂閱者。對比 LiveData, LiveData 內部有 version 的概念,對於註冊的訂閱者,會根據 version 進行判斷,將歷史數據發送給訂閱者。即所謂的“粘性”。我不認爲 “粘性” 是 LiveData 的設計缺陷,我認爲這是一種特性,有很多場景確實需要用到這種特性。StateFlow 則沒有此特性。

那總不能需要用到這種特性的時候,我又使用 LiveData 吧?下面要說的 SharedFlow 就是用來解決此種場景的。

3.2 SharedFlow

如果只是需要管理一系列狀態更新(即事件流),而非管理當前狀態.則可以使用 SharedFlow 共享流。如果對發出的一連串值感興趣,則這API十分方便。相比 LiveData 的版本控制,SharedFlow 則更靈活、更強大。

SharedFlow 也有兩種類型:SharedFlow 和 MutableSharedFlow:

public interface SharedFlow<out T> : Flow<T> {
   public val replayCache: List<T>
}

interface MutableSharedFlow<T> : SharedFlow<T>, FlowCollector<T> {
   suspend fun emit(value: T)
   fun tryEmit(value: T): Boolean
   val subscriptionCount: StateFlow<Int>
   fun resetReplayCache()
}

SharedFlow 是一個流,其中包含可用作原子快照的 replayCache。每個新的訂閱者會先從replay cache中獲取值,然後才收到新發出的值。

MutableSharedFlow可用於從掛起或非掛起的上下文中發射值。顧名思義,可以重置 MutableSharedFlow 的 replayCache。而且還將訂閱者的數量作爲 Flow 暴露出來。

實現自定義的 MutableSharedFlow 可能很麻煩。因此,官方提供了一些使用 SharedFlow 的便捷方法:

public fun <T> MutableSharedFlow(
   replay: Int,   // 當新的訂閱者Collect時,發送幾個已經發送過的數據給它
   extraBufferCapacity: Int = 0,  // 減去replay,MutableSharedFlow還緩存多少數據
   onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND  // 緩存溢出時的處理策略,三種 丟掉最新值、丟掉最舊值和掛起
): MutableSharedFlow<T>

MutableSharedFlow 的參數解釋在上面對應的註釋中。

3.2.1 SharedFlow 基本使用

class SharedFlowTest {
    private val _state = MutableSharedFlow<Int>(replay = 3, extraBufferCapacity = 2)
    val state: SharedFlow<Int> get() = _state

    fun getApi(scope: CoroutineScope) {
        scope.launch {
            for (i in 0..20) {
                delay(200)
                _state.emit(i)
                println("send data: $i")
            }
        }
    }
}

fun main() = runBlocking<Unit> {
    val test: SharedFlowTest = SharedFlowTest()

    test.getApi(this) // 開始獲取結果

    val job = launch(Dispatchers.IO) {
        delay(3000)
        test.state.collect {
            println("---collect1: $it")
        }
    }
    delay(5000)
    job.cancel()  // 取消任務, 避免泄漏
}

輸出結果如下:

send data: 0
send data: 1
send data: 2
send data: 3
send data: 4
send data: 5
send data: 6
send data: 7
send data: 8
send data: 9
send data: 10
send data: 11
send data: 12
send data: 13
---collect1: 11
---collect1: 12
---collect1: 13
send data: 14
---collect1: 14
send data: 15
---collect1: 15
send data: 16
---collect1: 16
send data: 17
---collect1: 17
send data: 18
---collect1: 18
send data: 19
---collect1: 19
send data: 20
---collect1: 20

分析一下該結果:
SharedFlow 每 200ms 發射一次數據,總共發射 21 個數據出來,耗時大約 4s。
SharedFlow 的 replay 設置爲 3, extraBufferCapacity 設置爲2, 即 SharedFlow 的緩存爲 5 。緩存溢出的處理策略是默認掛起的。
訂閱者是在 3s 之後開始手機數據的。此時應該已經發射了 14 個數據,即 0-13, SharedFlow 的緩存爲 8, 緩存的數據爲 9-13, 但是,只給訂閱者發送 3 箇舊數據,即訂閱者收集到的值是從 11 開始的。

3.2.2 MutableSharedFlow 的其它接口

MutableSharedFlow 還具有 subscriptionCount 屬性,其中包含處於活躍狀態的收集器的數量,以便相應地優化業務邏輯。
MutableSharedFlow 還包含一個 resetReplayCache 函數,在不想重放已向數據流發送的最新信息的情況下使用。

3.3 StateFlow 和 SharedFlow 的使用場景

StateFlow 的命名已經說明了適用場景, StateFlow 只會向訂閱者發射最新的值,適用於對狀態的監聽。
SharedFlow 可以配置對歷史發射的數據進行訂閱,適合用來處理對於事件的監聽。

3.4 將冷流轉換爲熱流

使用 sharedIn 方法可以將 Flow 轉換爲 SharedFlow。詳細介紹見下文。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章