轻松搞定Kotlin的Flow, ChannelFlow和CallbackFlow - 2

4,999 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情

续接上文: 轻松搞定Kotlin的Flow, ChannelFlow和CallbackFlow - 1

ChannelFlow

现在我们已经了解了Flow的缓冲区, 让我们看看Flow和ChannelFlow之间有什么区别.

1. ChannelFlow是有缓冲区的Flow

如果我们的代码如下:

fun main() = runBlocking {
    channelFlow  {
 for (i in 1..5) {
            println("Emitting $i")
            send(i)
        }
    }
 .collect { value ->
 delay(100)
            println("Consuming $value")
        }
} 

你能看到结果如下:

Emitting 1
Emitting 2
Emitting 3
Emitting 4
Emitting 5
Consuming 1
Consuming 2
Consuming 3
Consuming 4
Consuming 5

看起来像常规的flow使用了.buffer(3). 但是它有更多的缓冲, 默认大小是64.

1_bZxMWDDBrM4NWLsxWbpAhg.webp

要想增(或减)缓冲区, 可以在channelFlow上使用buffer.

2. ChannelFlow能够执行异步处理(比如合并)

假设我有下面的输入flow:

val myAbcFlow = flow { ('A'..'E').forEach {
 delay(50)
    emit(it)
 } }

val my123Flow = flow { (1..5).forEach {
 delay(50)
    emit(it)
 } } 

我计算用下面的代码来合并该流:

fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow  {
 collect { emit(it) }
 other.collect { emit(it) }
} 

当我合并它们的时候:

my123Flow.flowMerge(myAbcFlow).collect {
 delay(50)
    print("$it ")
 } 

结果如下:

1 2 3 4 5 A B C D E

1_SHmPZK1bHZ5OHf9kAEp5ng.webp

发谢的顺序与合并过程中的顺序一样. collect { emit(it) }必须在执行other.collect { emit(it) }之前完成.

我们不能让它用launch异步处理, 因为它不是CoroutineScope的一部分.

fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow  {
 launch { // ERROR HERE. 
 collect { emit(it) }
    }
 other.collect { emit(it) }
} 

我们甚至不能自作聪明, 试图做以下的事情:

fun <T> Flow<T>.flowMerge(other: Flow<T>): Flow<T> = flow  {
    CoroutineScope(Dispatchers.Main).launch {
       collect { emit(it) }
    }
    other.collect { emit(it) }
} 

它可以编译, 但在运行时将出错:

FlowCollector is not thread-safe and concurrent emissions are prohibited.
To mitigate this restriction please use 'channelFlow' builder instead of 'flow'

有了这个, channelFlow就发挥作用了.

fun <T> Flow<T>.channelMerge(other: Flow<T>): Flow<T> = channelFlow{
 launch { // THIS IS OKAY
 collect { send(it) }

    }
 other.collect { send(it) }
} 

我们将得到结果如下:

A 1 B 2 C 3 D 4 E 5

1_sKyJdZ6ZuZxcBivxlnnmCw.webp

下面的收集操作将不再阻塞:

launch { 
 collect { send(it) }
} 

由此其它的收集操作可以并行完成:

other.collect { send(it) } 

因此加快了整个过程, 同时使合并工作交替进行.

3. ChannelFlow能够保持开放

当我们执行以下流程时:

flow { 
 for (i in 1..5) emit(i) 
 } .collect { println(it) } 

完成. 结束.

同样地, 我们也可以将此应用于ChannelFlow:

channelFlow { 
 for (i in 1..5) send(i) 
 } .collect { println(it) } 

完成. 结束.

如果数据来自另一个来源, 而我们想保持流量开放呢? 你想让它变得"更热", 也就是说, 数据可以在上面的ChannelFlow lambda函数之外继续来.

1_xmjuJxt2I6SfpEsKgKgvbw.webp

在正常情况下, 这是不可能的.在完成Channel Flow lambda后, Channel Flow将被关闭.

然而, 对于Channel Flow, 我们可以在Lambda的末尾添加一个特殊的函数awaitClose().

channelFlow { 
 for (i in 1..5) send(i)
 awaitClose() 
 } .collect { println(it) } 

这将防止该函数在Lamda之后也被关闭.

为了扩展在channelFlow在lambda之后发送数据的能力, 我们可以将send函数分配给外面的一个变量函数, 如下所示, 即sendData:

fun main(): Unit = runBlocking {

    var sendData: suspend (data: Int) -> Unit = { }
    var closeChannel: () -> Unit = { }

    launch {
     channelFlow {
         for (i in 1..5) send(i)
         sendData = { data -> send(data) }
         closeChannel = { close() }
         awaitClose { 
             sendData = {}
            closeChannel = {}
         }
        } .collect { println(it) }
    }

     delay(10)
     println("Sending 6")
     sendData(6) 
     closeChannel() 
 } 

我们现在能够在channelFlow lambda之后sendData了.

我们也能够像上面展示的一样, 使用closeChannel函数舶关闭channel, closeChannel将在lambda内部调用close().

4. ChannelFlow能够发送非挂起数据

我们在用flow的时候, 必须得用emit, 因为它是挂起函数.

public suspend fun emit(value: T)

channelFlow中, 我们使用了send, 同样也是因为它是挂起函数.

public suspend fun send(element: E)

如果我们想发送不在暂停模式下的数据, 这是有限制的.

channelFlow中, 我们有trySend, 但它却不是挂起函数.

public fun trySend(element: E): ChannelResult<Unit>

它将允许人们在不需要挂起函数的情况下进行发送.

fun main(): Unit = runBlocking {

 var sendData: (data: Int) -> Unit = { } // Not suspending
 var closeChannel: () -> Unit = { }

 launch {
     channelFlow {
            for (i in 1..5) trySend(i)
            sendData = { data -> trySend(data) }
            closeChannel = { close() }
            awaitClose {
                sendData = {}
                closeChannel = {}
            }
        } .collect { println(it) }
    }

    delay(10)
    println("Sending 6")
    sendData(6)
    closeChannel()
    sendData(7)
 } 

注意, trySend在流被关闭的情况下很有用, 它将使用ChannelResult<Unit>返回状态而不是崩溃.

除了trySend之外, 还有trySendBlocking. 它是:

public fun <E> SendChannel<E>.trySendBlocking(element: E) : ChannelResult<Unit> {
    /*
     * Sent successfully -- bail out.
     * But failure may indicate either that the channel it full 
     * or that
     * it is close. Go to slow path on failure to simplify the 
     * successful path and
     * to materialize default exception.
     */
    trySend(element).onSuccess { return ChannelResult.success(Unit) }
       return runBlocking {
         val r = runCatching { send(element) }
         if (r.isSuccess) ChannelResult.success(Unit)
         else ChannelResult.closed(r.exceptionOrNull())
    }
}

借此:

  • trySend如果缓冲区满了, 将不发送数据, 并执行下一次trySend.
  • trySendBlocking如果缓冲区满了, 将不发送数据, 而是等待缓冲区可用时再trySendBlocking.

CallbackFlow

对于channelFlow, 当我们使用awaitClose()时, 表明我们希望它的行为是对其lambda的回调.

如果我们需要设置任何回调, 对我们来说, 设置awaitClose()是很重要的.

为了确保我们记得使用awaitClose(), 我们可以使用callbackFlow来代替channelFlow, 而callbackFlow本质上是channelFlow, 并强制使用awaitClose().

如果我们不对callbackFlow使用awaitClose(), 你会得到一个运行时错误, 即:

Exception in thread "main" java.lang.IllegalStateException: 'awaitClose { yourCallbackOrListener.cancel() }' should be used in the end of callbackFlow block.
Otherwise, a callback/listener may leak in case of external cancellation.

如果channelFlow比flow好得多, 为什么还要用flow?

最初, 我在想, flow只能做channelFlow能做的事情的一个子集. 认为channelFlowflow的一个超集.

然后我了解到flow在默认情况下与channelFlow的行为略有不同, 正如下面的文章所描述的那样:

Kotlin的ChannelFlow与Flow并不相同

它们也许相似, 但行为确不相同

除此之外, 与channelFlow相比, flow的重量也更轻.

如果我们只用emitsend, 来比较

fun main() = runBlocking {
 val x = 10000000
    val time = measureNanoTime {
        channelFlow  {
 for (i in 1..x) {
                send(i)
            }
        } .collect {}
    }
 println(time)
 } 

fun main() = runBlocking {
 val x = 10000000
    val time = measureNanoTime {
        flow  {
 for (i in 1..x) {
                emit(i)
            }
        } .collect {}
    }
 println(time)
 } 

结果如下:

1_qhStpAqJU49-VnBhOO5jTw.webp

很明显, 我们可以看到, flow在默认情况下比channelFlow重量轻.

所以简而言之, 使用flow, 除非你需要channelFlow的铃声和口哨.

TL; DR;

如果flow满足你的需要, 就使用flow.

如果你在flow内需要缓冲区和异步, 使用channelFlow.

如果你需要在flow之外发送数据, 使用callbackFlowawaitClose.

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情