第一节:Flow的基础知识

前言:

第一次完整的看完了协程的源码,并写成了一个专栏。虽然效果不是很理想,但是对我个人来说收益还是很大的。总结了一些在写协程有关知识失败的地方,希望在Flow的文章上能够带来一些突破。注重基础知识的讲解,而且一定要严谨。如果对协程感兴趣的同学,可以看看以下几篇文章。

第一节:协程的基础知识

第二节:深入理解协程上下文

第三节:源码解析协程的取消和异常流程 + 图解

第四节:协程取消和异常的运用

第五节:生产者消费者模型Channel的使用

1.流的概念

Kotlin Flow 是 Kotlin 提供的一种用于异步数据流处理的库。它类似于 RxJava 中的 Observable,可以用于处理异步数据流,并支持在数据流中进行操作、转换和组合。使用 Kotlin Flow 可以更方便地处理异步操作,避免回调地狱等问题。您可以使用 Kotlin Flow 来处理诸如网络请求、数据库查询、UI 事件等异步操作的数据流。

2.冷流和热流的区别

  1. 冷流: 冷流在被订阅前不会开始执行数据的生成。 它是惰性的,只有当观察者订阅时才会开始收集和发射数据。 每个订阅者都会收到独立的数据流,即每个订阅者触发的数据收集过程是独立的。
  2. 热流: 热流一旦开始执行,就会持续不断地产生数据,不论是否有订阅者。 数据的产生不是基于订阅者的存在,而是基于某种外部事件或条件。 所有订阅者会共享同一数据流,即一旦数据开始产生,所有订阅者都能接收到相同的数据。 在KotlinFlow API中,可以通过shareInstateIn 等操作符将冷流转换为热流。

3.冷流的创建方式

冷流的创建方式有很多种。最常见的创建方式,是使用顶层函数flow()或者flowOf()来创建。如下代码示列:

从输出结果不难看出,冷流是自下而上触发数据的发送。只有当我们调用末端操作符collect()时,flow()函数中的Lambda实例才会被调用,从而触发emit()。 其中flowOf()被定义在协程库kotlinx.coroutines.flow包下的Builder.kt文件中,内部已经帮我做了emit()操作,所以我们无需再重复调用emit()函数。

该文件中还为我们提供了迭代器、序列、挂起函数类型的顶层扩展函数asFlow(),我们可以很方便的将这些对象转化为一个Flow对象。

scss 复制代码
fun main() {
    runBlocking {
        listOf(1, 2, 3).asFlow().collect { value ->
            print("receiver: value = $value\n")
        }
        arrayOf(1, 2, 3).asFlow()
        mapOf(1 to 1, 2 to 2, 3 to 3).asIterable().asFlow()
    }
}

// 输出
receiver: value = 1
receiver: value = 2
receiver: value = 3

4.MutableSharedFlow

Kotlin flow为开发者提供了两种创建热流的顶层函数:MutableSharedFlow()和MutableStateFlow()。在函数式编程中相信大家都已经司空见惯了,我们经常定义一个顶层函数来伪装成同名类型的构造器。

  1. 顶层函数MutableSharedFlow()

主要有三个参数:

  • replay: 新订阅者 collect 时,发送 replay 个历史数据给它,默认新订阅者不会获取以前的数据。
  • extraBufferCapacity: MutableSharedFlow 缓存的数据个数为 replay + extraBufferCapacity; 缓存一方面用于粘性事件的发送,另一方面也为了处理背压问题,既下游的消费者的消费速度低于上游生产者的生产速度时,数据会被放在缓存中。
  • onBufferOverflow: 背压处理策略,缓存区满后怎么处理(挂起或丢弃数据),默认挂起。注意,当没有订阅者时,只有最近 replay 个数的数据会存入缓存区,不会触发 onBufferOverflow 策略。以下是对BufferOverflow定义的几种类型的具体说明。
csharp 复制代码
public enum class BufferOverflow {
    /**
     * 缓冲区溢出时挂起
     */
    SUSPEND,

    /**
     * 在溢出时删除缓冲区中最早的值,将新值添加到缓冲区,不挂起
     */
    DROP_OLDEST,

    /**
     * 在缓冲区溢出时删除当前添加到缓冲区的最新值(以便缓冲区的内容不变),不要挂起。
     */
    DROP_LATEST
}  

简单示例:

java 复制代码
fun main() {
    val sharedFlow = MutableSharedFlow<String>(replay = 2)
    scope.launch {
        for (value in 0 until 10) {
            delay(100)
            sharedFlow.emit("$value")
            println("emit value = $value")
        }
    }

    scope.launch {
        sharedFlow.collect { value ->
            // 消费速度慢于生产速度
            delay(200)
            println("receiver value = $value")
        }
    }
    runBlocking { delay(10000) }
}
// 输出
emit value = 0
emit value = 1
receiver value = 0
emit value = 2
emit value = 3
receiver value = 1
emit value = 4
receiver value = 2
emit value = 5
receiver value = 3
emit value = 6
receiver value = 4
emit value = 7
receiver value = 5
emit value = 8
receiver value = 6
emit value = 9
receiver value = 7
receiver value = 8
receiver value = 9

2.扩展函数shareIn

kotlin 复制代码
public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T> {
    val config = configureSharing(replay)
    val shared = MutableSharedFlow<T>(
        replay = replay,
        extraBufferCapacity = config.extraBufferCapacity,
        onBufferOverflow = config.onBufferOverflow
    )
    @Suppress("UNCHECKED_CAST")
    val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T)
    return ReadonlySharedFlow(shared, job)
}

shareIn是Flow的一个扩展函数,主要有3个参数:

  • scope: CoroutineScope,定义了 Flow 的生命周期。
  • started: SharingStarted,定义了 Flow 开始共享的条件。有以下几种选项:
  1. Eagerly: 立即启动 Flow,即使没有订阅者。
  2. Lazily: 当存在首个订阅者时启动
  3. WhileSubscribed(replayExpiration: Duration = Duration.INFINITE, stopTimeout: Duration = 0.seconds): 当有订阅者时启动 Flow,并且在最后一个订阅者取消订阅后,等待 stopTimeout 时间后停止 Flow。replayExpiration 定义了重新播放缓存值的时间。
  • replay: 缓存的元素数量,当新的订阅者加入时,可以重放这些缓存的值

实际开发中,我们可以根据需求择优选择设置started的类型

简单示例:

scss 复制代码
fun main() = runBlocking {
    val flow = (1..3).asFlow().onEach { delay(1000) } // 模拟耗时操作

    val sharedFlow = flow.shareIn(this, SharingStarted.Lazily, 1)

    launch {
        sharedFlow.collect { value ->
            println("Collector 1: $value")
        }
    }

    launch {
        delay(500) // 确保第一个收集器已经开始
        sharedFlow.collect { value ->
            println("Collector 2: $value")
        }
    }

    delay(4000) // 等待所有收集完成
}
// 输出
Collector 1: 1
Collector 2: 1
Collector 1: 2
Collector 2: 2
Collector 1: 3
Collector 2: 3

5. 顶层函数MutableStateFlow()

1.StateFlow是粘性的,而SharedFlow是非粘性的。
粘性事件/状态 :即当新订阅者开始监听时,会立即收到最后一次发射的值。
非粘性事件/状态:新订阅者只会在有新值发射时收到更新,不会立即收到历史值。 理解了粘性和非粘性的概念后,下面我们使用一下代码示例来验证两者的区别:

kotlin 复制代码
private val scope = CoroutineScope(Dispatchers.Default)

fun main() {
    val stateFlow = MutableStateFlow("-1")
    val sharedFlow = MutableSharedFlow<String>()
    scope.launch {
        for(value in 0 until 3) {
            stateFlow.emit("$value")
            sharedFlow.emit("$value")
            println("emit value = $value")
        }
    }

    scope.launch {
        sharedFlow.collect { value ->
            println("sharedFlow collect value = $value")
        }
    }

    scope.launch {
        stateFlow.collect { value ->
            println("stateFlow collect value = $value")
        }
    }

    runBlocking { delay(500) }
}

// 输出

emit value = 0
emit value = 1
emit value = 2
stateFlow collect value = 2

从输出结果来看,sharedFlow并没有输出,在emit操作执行完成后,在后续的订阅中并没有收到历史值。而上文中我们又说到顶层函数MutableSharedFlow()它有一个replay参数。(replay: 新订阅者 collect 时,发送 replay 个历史数据给它,默认新订阅者不会获取以前的数据。)也就是说如果我们在SharedFlow创建时将replay初始值设为1,那么我们就可以将SharedFlow当做StateFlow来使用。我们只需将上述示列中创建SharedFlow的代码做如下改动:

ini 复制代码
val sharedFlow = MutableSharedFlow<String>()

val sharedFlow = MutableSharedFlow<String>(replay = 1)

运行上述代码,我们就可以得到以下结果:

ini 复制代码
emit value = 0
emit value = 1
emit value = 2
sharedFlow collect value = 2
stateFlow collect value = 2

2.扩展函数stateIn

kotlin 复制代码
public fun <T> Flow<T>.stateIn(
    scope: CoroutineScope,
    started: SharingStarted,
    initialValue: T
): StateFlow<T> {
    val config = configureSharing(replay = 1)
    val state = MutableStateFlow(initialValue)
    val job = scope.launchSharing(config.context, config.upstream, state, started, initialValue)
    return ReadonlyStateFlow(state, job)
}

从stateIn的实现,我们可以看出它和扩展函数shareIn很相似。不同的是第三个参数initiaValue,stateIn方法需要提供一个初始数据值,并在函数内部将可接受的历史数据replay设置了为1。

6.总结

到这里关于Flow的基础知识我们就介绍完了,关于Flow的底层实现还是有些复杂的,这里就不展开描述了。下篇文章,我们将介绍Flow的一些基础操作符,感谢您的观看~

相关推荐
腹黑天蝎座3 小时前
浅谈React19的破坏性更新
前端·react.js
东华帝君3 小时前
react组件常见的性能优化
前端
第七种黄昏3 小时前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ3 小时前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript
林希_Rachel_傻希希3 小时前
JavaScript 解构赋值详解,一文通其意。
前端·javascript
Yeats_Liao3 小时前
Go Web 编程快速入门 02 - 认识 net/http 与 Handler 接口
前端·http·golang
金梦人生3 小时前
🔥Knife4j vs Swagger:Node.js 开发者的API文档革命!
前端·node.js
东华帝君3 小时前
react 虚拟滚动列表的实现 —— 固定高度
前端
Larcher3 小时前
n8n 入门笔记:用零代码工作流自动化重塑效率边界
前端·openai