Kotlin Flow 入门:构建响应式异步数据流

Flow 的定位:协程版的 Sequence

要理解 Flow 是什么,先要理解 Sequence(序列)。

SequenceQueue 一样,都可用于按顺序提供数据,但它并不是一种数据结构,而是一种机制。

为什么这么说呢?

它内部没有存储数据,只是存储了提供数据的规则。当需要提供数据时,会动态生产,并且是"用完一条才生产下一条"。

kotlin 复制代码
fun main() = runBlocking<Unit> {

    val numSeq = sequence { // 创建 Sequence 对象
        println("Producing 1")
        yield(1) // 生产数据
        println("Producing 2")
        yield(2)
        println("Producing 3")
        yield(3)
    }

    launch {
        for (num in numSeq) {
            println("Consuming $num")
            delay(1000)

            if (num == 2) {
                break
            }
        }
    }

}

运行结果:

less 复制代码
Producing 1
Consuming 1
Producing 2
Consuming 2

可以看到,因为提前结束了循环,所以并不会打印 "Producing 3",这展示了 Sequence 的惰性求值特性。

并且 SequenceList 一样,都可用来处理数据集合。不过 List 会立即执行每个操作,中间会产生新的 List。而 Sequence 是惰性求值,它会先构建一个操作链,然后让生产的每个数据执行完所有操作步骤。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // Sequence 惰性求值
    val seq = sequence { 
        println("Sequence producing 1")
        yield(1) 
        println("Sequence producing 2")
        yield(2)
    }

    seq.take(1).forEach {
        println("Consuming Sequence: $it")
    }

    // List 立即求值
    val list = buildList {
        println("List producing 1")
        add(1)
        println("List producing 2")
        add(2)
    }
    list.take(1).forEach { println("Consuming List: $it") }
}

运行结果:

less 复制代码
Sequence producing 1
Consuming Sequence: 1
List producing 1
List producing 2
Consuming List: 1

这同样清晰地展示 Sequence 按需生产的惰性特性。

那么 Sequence 有什么用处吗?

关键在于它是惰性的,它的生产策略是消极的。如果遍历过程中,停止遍历,这样可以减少生产耗时。如果数据的获取是持续的,只需要这样:

kotlin 复制代码
fun main() = runBlocking<Unit> {

    val numSeq = sequence {
        while (true) {
            yield(getData()) // 实际上这行会报错
        }
    }

    launch {
        for (num in numSeq) {
            println("Consuming $num")
            delay(1000)
        }
    }

}

// 模拟耗时的挂起操作
suspend fun getData(): Int = withContext(Dispatchers.IO) {
    delay(1000)
    Random.Default.nextInt()
}

虽然 sequence 构建器代码块内部允许我们使用挂起函数,比如调用 yield() 生产数据。但由于 SequenceScope,我们只能调用这个作用域内部的 yieldyieldAll 挂起函数(因为 @RestrictsSuspension 注解),并不能调用 delay 等挂起函数(包括我们之前定义的 getData())。

kotlin 复制代码
@SinceKotlin("1.3")
@Suppress("DEPRECATION")
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }

flow() 中拥有完整的挂起环境,所以说:Sequence 的定位是一个惰性的同步数据序列(无法调用外部的挂起函数),Flow 的定位则是一个支持挂起函数的、惰性的异步数据流

kotlin 复制代码
fun main() = runBlocking<Unit> {

    val numSeq = flow {
        while (true) {
            // 发送数据
            emit(getData())
        }
    }

    launch {
        numSeq.collect { // 遍历 Flow
            println("Consuming $it")
            delay(1000)
        }
    }

}

suspend fun getData(): Int = withContext(Dispatchers.IO) {
    delay(1000)
    Random.Default.nextInt() 
}

运行结果:

less 复制代码
Consuming -680817724
Consuming 611886988
Consuming -695569431
...

冷流:Flow 的核心原理

现在,我们来讲讲 Flow 的核心原理 "冷流"。

首先有个问题:Flow 生产了第一条数据后,在新的协程中收集它,那么在这个协程中能不能获取到第一条数据?

kotlin 复制代码
val myFlow = flow {
    emit(1)
    delay(1000)
    emit(2)
}
fun main() = runBlocking<Unit> {

    println("Calling collect A...")
    launch {
        myFlow.collect {
            println("A received $it")
        }
    }

    delay(500)

    println("Calling collect B...")
    launch {
        myFlow.collect {
            println("B received $it")
        }
    }

}

运行结果:

less 复制代码
Calling collect A...
A received 1
Calling collect B...
B received 1
A received 2
B received 2

flow{...} 中的代码提供数据流的生产逻辑,在定义时并不会执行任何操作,只有当 collect 被调用时,它才会被执行。并且每一次调用 collect,都会触发一套全新、独立的生产流程。

实际上,launch { myFlow.collect { println("A received $it") } } 在概念上等同于:

kotlin 复制代码
launch {
    // Flow 代码块开始
    val value1 = 1 // 对应 emit(1)
    println("A received $value1") // 对应 collect 代码块的执行

    delay(1000)

    val value2 = 2
    println("A received $value2")
    // Flow 代码块结束
}

每一个 collect 调用都相当于执行了这样一套独立的逻辑。

这就是冷流,它与热流(如 ChannelSharedFlow)不同,热流的数据生产流程是独立的,与收集者无关。

Flow 的应用场景:解耦

当需要处理数据流时,就需要用到 Flow 吗?

不是的,我们只需要循环获取数据,然后处理即可。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    launch {
        while (isActive) {
            val data = getData()
            processData(data)
        }
    }
}

suspend fun getData() = withContext(Dispatchers.IO) {
    "data"
}

fun processData(data: String) {
    println(data)
}

但如果要将数据的获取和处理功能拆分,就需要用到 Flow,例如:

kotlin 复制代码
// 生产者
fun fetchWeatherUpdates(): Flow<String> = flow {
    val weather = listOf("sunny", "cloudy", "rainy")
    while (true) {
        val latestWeather = withContext(Dispatchers.IO) {
            delay(500)
            weather.random()
        } // 挂起函数
        emit(latestWeather)
        delay(60_000) // 每分钟更新
    }
}

// 消费者
suspend fun getWeatherUpdates(weatherFlow: Flow<String>) {
    weatherFlow.collect { weather ->
        println("Now weather is $weather") // 更新 UI
    }
}

fun main() = runBlocking<Unit> {
    val flow: Flow<String> = fetchWeatherUpdates()
    launch {
        getWeatherUpdates(flow)
    }
}

消费者并不需要关心数据的来源,生产者不关心数据的处理。

Flow 的创建

除了之前的 flow() 外,Flow 最常见的创建方式还有 flowOfasFlow

flowOf()

它会将你提供的一组数据转换为 Flow,并将这些数据一次性发送出来。例如:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // 创建按顺序发送 1, 2, 3 的 Flow
    val flow = flowOf(1, 2, 3)
    flow.collect {
        println(it)
    }
}

运行结果:

less 复制代码
1
2
3

我们来看看 flowOf 的内部实现,发现它只是在 flow{...} 的基础上,将传入的每个元素调用 emit() 发射出去罢了。

kotlin 复制代码
// Builders.kt
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
    for (element in elements) {
        emit(element)
    }
}

asFlow()

asFlow() 是一个扩展函数,它可以将现有的集合或序列转换为 Flow,原理和 flowOf 类似。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    // 将 List 转换为 Flow
    listOf("A", "B", "C").asFlow()
        .collect { println(it) }

    // 将 Sequence 转换为 Flow
    sequenceOf("X", "Y", "Z").asFlow()
        .collect { println(it) }
}

运行结果:

less 复制代码
A
B
C
X
Y
Z

consumeAsFlow() 和 receiveAsFlow()

这两个函数可以将 Channel 转为 Flow

但由于背后生产数据的是 Channel,所以数据会瓜分给所有的消费者。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    val channel = Channel<Int>()
    launch {
        var count = 0
        while (isActive) {
            channel.send(count++)
            delay(500)
        }
    }

    delay(2000)

    val flow = channel.receiveAsFlow()
    launch {
        flow.collect {
            println("A received: $it")
        }
    }

    delay(500)

    launch {
        flow.collect {
            println("B received: $it")
        }
    }

    delay(2000)
    channel.cancel()
}

运行结果:

less 复制代码
A received: 0
A received: 1
B received: 2
A received: 3
B received: 4

由于数据被瓜分,所以当 collect 的调用超过一次时,会导致每个消费者接收不到完整的数据序列。

所以有了 consumeAsFlow(),当它创建出的 Flow 调用 collect 被收集时,内部会标记为已消费,如果再次调用 collect 去收集,会抛出 IllegalStateException 异常。

例如将上述代码中的 .receiveAsFlow() 换成 .consumeAsFlow(),运行结果会是:

less 复制代码
A received: 0
Exception in thread "main" java.lang.IllegalStateException: ReceiveChannel.consumeAsFlow can be collected just once

这个异常相当于是一种提醒。

channelFlow 和 callbackFlow

channelFlow() 也可创建 Flow,它使用 Channel 来生产数据。

receiveAsFlow() 不同的是:直到 collect 收集时,它才会创建 Channel 对象开始生产数据,也就是说,多次创建便会创建多个 Channel,所以它创建的也是"冷流"。

kotlin 复制代码
fun main() = runBlocking<Unit> {
    val flow = channelFlow {
        for (i in 1..5) {
            send(i) // 内部使用的是 Channel,所以调用 Channel 的 send 方法来生产
        }
    }

    launch {
        flow.collect {
            println(it)
        }
    }
}

不过,channelFlow() 的关键用途并不在于创建 Flow。在了解它的用途前,我们先来看看 Flowemit 的跨界问题。

emit 的跨界问题

如果在 flow 的代码块中启动一个新的协程来 emit 数据,会抛出 IllegalStateException 异常,简略的异常信息为:

less 复制代码
Flow invariant is violated:
Emission from another coroutine is detected.

为什么会有这个限制?

这是为了保护消费者的上下文,让代码的行为逻辑与开发者预期一致。

kotlin 复制代码
val myFlow = flow {
    withContext(Dispatchers.IO) {
        emit(1) // 在 IO 线程发射
    }
}

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default) { // 请假设这是主线程 Dispatchers.Main
        myFlow.collect { data ->
            // 我们期望这里是主线程,但实际却运行在 IO 线程
            println(data) // 更新 UI
        }
    }
}

我们知道 flow{...} 代码块中的代码会放在 collect 中执行,如果不进行限制,就可能会导致上述问题:collect 中更新 UI 的代码运行在了 Dispatchers.IO 线程,应用崩溃了。

为此,flow 块中的 emit 必须运行在调用 collect 的那个协程的上下文中,这样让 Flow 变得更加安全以及更加符合开发者的直觉。

channelFlow 的关键用途:跨协程生产

再说回 channelFlow,它的关键用途在于它允许我们跨界,也就是允许在不同的协程中生产数据。

为什么它能做到?因为 Channel 本身就是一个可跨协程通信的组件。

kotlin 复制代码
val myFlow = channelFlow {
    withContext(Dispatchers.IO) {
        send(1) // 在 IO 线程发射
    }
}

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default) { // 请假设这是主线程
        myFlow.collect { data ->
            println(data) // 完全可以安全地更新 UI
        }
    }
}

callbackFlow:对接传统回调

channelFlow 还可以用来对接传统的回调,只需这样:

kotlin 复制代码
fun LocationManager.locationFlow(): Flow<Location> = channelFlow {
    // 定义回调
    val listener = object : LocationListener {
        override fun onLocationChanged(location: Location) {
            // 在回调中使用 trySend 发送数据,因为当前并没有在协程环境中,所以不能使用 send
            trySend(location)
        }
        
        override fun onProviderDisabled(provider: String) {
            // 当服务不可用时,关闭 Channel
            close(IllegalStateException("Provider $provider disabled"))
        }
    }

    // 注册回调
    requestMyLocationUpdates(listener)

    // 挂起生产者协程,直到 Flow 被外部取消
    awaitClose {
        // 在这里执行清理工作:注销回调
        removeMyLocationUpdates(listener)
    }
}

为什么需要 awaitClose()

channelFlow 的代码块是一个协程,如果只是注册了回调,那么这个代码块会立即执行完毕并退出,Flow 也会随之关闭,回调也就不会被接收了。为此,我们必须调用 awaitClose(),它会将生产者协程挂起,直到 Flow 被消费者取消,或者被生产者关闭。

无论 Flow 是如何结束的,awaitClose 的代码块一定会被执行,我们可以在这执行清理逻辑。

callbackFlow 只是 channelFlow 的一个特殊版本,它强制要求调用 awaitClose()。如果不调用,会抛出异常。

Flow 的收集

collectIndexed

如果我们需要在收集数据时,知道当前数据的索引,可以使用 collectIndexed

kotlin 复制代码
fun main() = runBlocking<Unit> {
    flowOf("A", "B", "C").collectIndexed { index, value ->
        println("Index $index: Value $value")
    }
}

收集与 launchIn

前面我们已经知道 collect 是一个挂起函数,如果数据流是无限的,那么它会一直挂起。

这会导致后续代码无法执行:

kotlin 复制代码
val weatherFlow = flow {
    val weatherList = listOf("sunny", "cloudy", "rainy")
    while (true) {
        emit(weatherList.random())
        delay(1000)
    }
}
fun main() = runBlocking<Unit> {
    weatherFlow.collect {
        println(it)
    } // 将永远挂起

    // 这行代码永远不会执行
    println("Collection finished")
}

为此,我们应该为每一个 collect 启动一个单独的协程。

所以需要这样:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    launch {
        weatherFlow.collect {
            println(it)
        }
    }

    println("Collection finished")
}

launchIn 可用于让 collect 在指定的 scope 启动的协程中执行,我们来看看这个函数的内部实现:

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

很简单,就是使用了传入的 CoroutineScope 启动了一个新的协程,并在这个协程中调用了 collect()

我们常常会将它和 onEach() 中间操作符配合使用,下面的代码等价于之前的代码:

kotlin 复制代码
fun main() = runBlocking<Unit> {
    weatherFlow.onEach {
        println(it)
    }.launchIn(this)

    println("Collection finished")
}
相关推荐
阿里云云原生4 小时前
告别手动埋点!Android 无侵入式数据采集方案深度解析
android·云原生
shayudiandian4 小时前
【Kotlin】数组集合常用扩展函数
kotlin
Tlaster4 小时前
使用KMP实现原生UI + Compose混合的社交客户端
android·ios·开源
袁煦丞 cpolar内网穿透实验室5 小时前
安卓旧机变服务器,KSWEB部署Typecho博客并实现远程访问:cpolar内网穿透实验室第645个成功挑战
android·运维·服务器·远程工作·内网穿透·cpolar
游戏开发爱好者85 小时前
iOS 26 App 查看电池寿命技巧,多工具组合实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
用户41659673693555 小时前
基于Jetpack Compose 实现列表嵌套滚动联动机制 (完整源码解析)
android
林栩link5 小时前
【车载Android】使用自定义插件实现多语言自动化适配
android
消失的旧时光-194310 小时前
Flutter 响应式 + Clean Architecture / MVU 模式 实战指南
android·flutter·架构
404未精通的狗10 小时前
(数据结构)栈和队列
android·数据结构