瞅一眼Kotlin Flow

一、前言

Kotlin Flow是Kotlin中响应式编程框架的实现,是Kotlin生态中的一个重要组成部分,而提到响应式编程框架,作为Android开发的我们势必会联想RxJava,因其丰富、强大的功能,陡峭的学习曲线,让人又爱又恨。相较于RxJava,Flow的一个最大特点是其基于Kotlin协程,一个Flow必须运行在协程中,因此在Flow中我们能够利用协程提供的特性编写更加简洁、轻量的异步代码。

1、响应式编程

关于响应式编程,在我的理解中,它是一种面向异步 数据流的编程范式。其中数据流的概念很好理解,就是一组按时间顺序排列的数据序列;而异步是指数据的接收是通过注册的callback/observer/collector来完成的,典型的观察者模式。

正因为响应式编程面向的是异步数据流,而不是函数,所以能够在此之上抽象出一组强大的工具函数,让我们使用少量的代码便能实现复杂的业务逻辑,比如搜索框防抖,多个有依赖关系的网络请求等等。

这里给大家推荐一篇介绍响应式编程的优秀文章:

借用文章中的一个例子,假设我们现在想要统计用户在一次"连击"中对应的点击次数,把250ms内大于等于2次以上的点击视为一次"连击"。 在传统的编程模型下,我们势必要定义一些的变量来记录状态以及编写相应的定时逻辑,但是在响应式编程中,我们只需要极少量的代码便能完成上述逻辑。

通过下面的示意图来更直观地理解这一过程:

首先通过buffer(clickStream.throttle(250ms))将250ms内的产生的点击事件聚合成一个list,再使用map('get length of list')将list的数据流转换为list长度的数据流,最后过滤掉长度小于2的数据就得到了最终期望的数据流。

2、基本概念

在进一步之前,先介绍下Flow中的一些基本概念。在官方文档中,将Flow关联的各角色划分为三类:

  • 生产者(Producer):负责数据的生产、发送;
  • 中介(Intermediary):可选的,可以有若干个,负责对Flow中的数据,甚至是Flow本身的进行变换;
  • 消费者(Consumer):负责从Flow中接收数据。

中介其实就是各类中间操作符:mapfilter等,根据角色在数据流中的位置,我们将其上面的部分称作上游,下面的部分称作下游。

举一个例子🌰,假设我们有如下的数据流,Intermediary[i] 便是Producer->...->Intermediary[i - 1] 的下游、Intermediary[i + 1] ->...->Consumer的上游:

rust 复制代码
Producer->...->Intermediary[i- 1 ] ->Intermediary[i] ->Intermediary[i+ 1 ] ->...->Consumer

二、Flow的创建和使用

创建一个Flow非常简单,官方提供了一个顶层函数用于创建Flow,如下所示:

kotlin 复制代码
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>

这一顶层函数又被称作Flow builder ,其中有两个关键的接口:FlowFlowCollector

kotlin 复制代码
public interface Flow<out T> {
    public suspend fun collect(collector: FlowCollector<T>)
}
kotlin 复制代码
public fun interface FlowCollector<in T> {
    /**
     * Collects the value emitted by the upstream.
     * This method is not thread-safe and should not be invoked concurrently.
     */
    public suspend fun emit(value: T)
}

Flow描述一个异步数据流,类似于RxJava中的Observable,而FlowCollector负责数据的接收,类似于RxJava中的Observer

Flow的定义很简单,只有一个collect方法,接收一个FlowCollector对象作为参数。当调用collect方法时,会将定义好的FlowCollector对象传递至上游,上游会使用传递过来的FlowCollector对象的emit方法来发送数据。

回到Flow builder ,其接收一个使用suspend修饰,FlowCollector的扩展函数:block,又被称作Producer block ,负责数据的生产,并将生产好的数据通过FlowCollector对象的emit方法发送。

kotlin 复制代码
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T>

来看一个简单的例子:

kotlin 复制代码
private val myFlow = flow<Int> producer@{
    var value = 0
    while (true) {
        emit(value++)
        delay(1000)
    }
}

GlobalScope.launch {
    myFlow.collect(object : FlowCollector<Int> {
        override suspend fun emit(value: Int) {
            Log.i(TAG, "receive $value")
        }
    })
}

这里定义了一个每间隔1秒发送一次数据的Flow,因为Flow需要运行在协程中,所以通过GlobalScope.launch启动了一个协程,然后在协程中调用Flow对象的collect方法,并传递一个FlowCollector实例,在其emit方法中接收上游发送过来的数据。

需要注意的是,通过Flow Builder 创建的Flow属于冷流 ,意味着只有当Flow的终端操作函数被调用时,比如collect函数,才会执行生产者代码,也就是示例中的producer代码块,并且每次调用终端操作函数时都会创建一条新的数据流,彼此互不影响,在下面的示例中producer代码块会被执行两次,产生两条数据流:

kotlin 复制代码
GlobalScope.launch {
    myFlow.collect consumer1@{ value -> Log.i(TAG, "receive $value") }
}
GlobalScope.launch {
    myFlow.collect consumer2@{ value -> Log.i(TAG, "receive $value") }
}

此外,因为FlowCollectoremit方法并不是线程安全的,所以不允许我们在一个新的协程或者新的Dispatcher中调用,以下代码都是非法的:

kotlin 复制代码
private val myFlow1 = flow<Int> {
    GlobalScope.launch { 
        emit(1)
    }
}

private val myFlow2 = flow<Int> {
    withContext(Dispatchers.IO) {
        emit(1)
    }
}

1、操作符

1.1、生命周期

Flow中提供了onStartonCompletion操作符用于监听数据流的开始、结束,以onStart为例:

kotlin 复制代码
public fun <T> Flow<T>.onStart(
    action: suspend FlowCollector<T>.() -> Unit
): Flow<T>

参数action是一个使用suspend修饰,FlowCollector的扩展函数,其Receiver直接/间接地持有从下游传递过来的FlowCollector对象,允许我们调用emit方法向数据流的开头添加额外的数据,如下所示:

kotlin 复制代码
GlobalScope.launch {
    flowOf(1, 2)
        .onStart {
            emit(0)
        }
        .onCompletion {
            emit(3)
        }
        .collect {
            Log.i(TAG, "receive $it")
        }
}
// receive 0
// receive 1
// receive 2
// receive 3

1.2、线程切换

前面有提到,不允许在新起的协程/线程中调用FlowCollectoremit方法,如果需要显示地切换生产者代码运行的线程,需要借助flowOn操作符:

kotlin 复制代码
public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T>

private val myFlow = flow<Int> producer@{
    var value = 0
    while (true) {
        emit(value++)
        delay(1000)
    }
}

GlobalScope.launch(Dispatchers.Main) {
    myFlow.flowOn(Dispatchers.IO)
        .onCompletion completation@{
            Log.i(TAG, "on complete")
        }
        .collect consumer@{
            Log.i(TAG, "receive $it")
        }
}

flowOn操作符只会切换上游数据流的CoroutineContext,不会影响到下游数据流。所以在上述示例代码中,producer代码块会运行在子线程中,completation、consumer代码块会运行在主线程上。

1.3、异常处理

Flow中异常的处理是通过catch操作符来完成的,上游抛出的异常会被捕获,默认会结束流的执行:

kotlin 复制代码
GlobalScope.launch(Dispatchers.Main) {
    flowOf(1, 2, 3)
        .map {
            if (it == 2) {
                throw RuntimeException("error")
            }
            it
        }.flowOn(Dispatchers.IO)
        .catch { e ->
            Log.i(TAG, "catch exception, ${e.message}")
        }
        .collect {
            Log.i(TAG, "receive $it")
        }
}
// receive 1
// catch exception, error

2、callbackFlow

flow构建 api只适用于同步生产的数据,而针对异步返回的数据,官方提供了另一个顶层函数:

kotlin 复制代码
public fun <T> callbackFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit): Flow<T>

/**
 * Sender's interface to [Channel].
 */
public interface SendChannel<in E> {
    public suspend fun send(element: E)
    public fun trySend(element: E): ChannelResult<Unit>
    public fun close(cause: Throwable? = null): Boolean
    ......
}

block函数接收一个ProducerScope对象,其实现了SendChannel接口,允许我们通过sendtrySend方法发送数据。callbackFlow的底层基于Channel,概念上类似于Java中的BlockingQueue,不同之处在于它是非阻塞的,如果发送数据时缓冲区已满,会将发送方协程挂起并加入到内部维护的等待队列中,等待缓冲区有空闲时被唤醒。数据的接收也是类似的,如果当前缓冲区为空,会将接收方协程挂起并加入到等待队列中,等到有数据时被唤醒。

下面通过一个例子来给大家介绍一下callbackFlow的使用:

kotlin 复制代码
private val myCallbackFlow = callbackFlow {
    val callback = object : MyCallback {
        override fun onNext(value: Int) {
            trySend(value) // trySendBlocking(value)
        }

        override fun onCompletion() {
            close()
        }

        override fun onFailure(throwable: Throwable?) {
            cancel()
        }
    }
    api.register(callback)

    awaitClose { api.unregister(callback) }
}

GlobalScope.launch { 
    myCallbackFlow.collect {
        Log.i(TAG, "receive $it") 
    }
}

trySend方法是send方法的非挂起版本,允许我们在协程外、非挂起函数中调用,但是它不能保证数据的发送,当缓冲区已满时,发送的数据会被丢弃掉。如果我们需要保证数据的发送,可以调用trySendBlocking方法,当缓冲区已满时,会堵塞当前线程直至成功发送数据或者对应的Channel、协程被关闭、取消。

需要注意的是,awaitClose方法被强制要求调用,否则会抛出IllegalStateException异常,这是为了避免因协程结束导致我们注册的callback泄露。awaitClose方法会将当前协程挂起,并保证在Channel关闭或者协程取消时执行我们传递的代码块,从而完成相关资源的释放。还有一点是,传递给awaitClose方法的代码块不能保证和传递给callbackFlow方法的代码块在同一个线程执行,因此相关的注册、反注册方法需要是线程安全的。

参考

  1. The introduction to Reactive Programming you've been missing
  2. Kotlin flows on Android
  3. Kotlin Flow 实际运用
  4. Kotlin 异步 | Flow 应用场景及原理
  5. Kotlin 异步 | Flow 限流的应用场景及原理
相关推荐
Kapaseker17 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
A0微声z3 天前
Kotlin Multiplatform (KMP) 中使用 Protobuf
kotlin
alexhilton3 天前
使用FunctionGemma进行设备端函数调用
android·kotlin·android jetpack
lhDream4 天前
Kotlin 开发者必看!JetBrains 开源 LLM 框架 Koog 快速上手指南(含示例)
kotlin
RdoZam4 天前
Android-封装基类Activity\Fragment,从0到1记录
android·kotlin
Kapaseker4 天前
研究表明,开发者对Kotlin集合的了解不到 20%
android·kotlin
糖猫猫cc4 天前
Kite:两种方式实现动态表名
java·kotlin·orm·kite
如此风景5 天前
kotlin协程学习小计
android·kotlin
Kapaseker5 天前
你搞得懂这 15 个 Android 架构问题吗
android·kotlin
zh_xuan5 天前
kotlin 高阶函数用法
开发语言·kotlin