Kotlin Flow的使用


在 Kotlin Coroutines 中,Flow 有两种基本类型:冷流(Cold Flow)和 热流(Hot Flow)。stateIn 操作符用于将冷流(Cold Flow)转换成热流(Hot Flow)。我们来深入解释一下这两种类型及 stateIn 的作用。

1. 冷流(Cold Flow)

定义:冷流是一种延迟的流,只有当被订阅(即调用 collect() 方法)时才会开始执行和发射数据。

特性:

每个订阅者都会独立收集数据:每次 collect 操作都会从头开始执行流中的代码,等于重新启动整个数据流。
没有订阅时不产生数据:冷流在没有订阅者时不会主动发射任何数据。
例子:

val coldFlow = flow {
    println("Emitting data")
    emit(1)
    emit(2)
    emit(3)
}

coldFlow.collect { println(it) }  // 订阅者1
coldFlow.collect { println(it) }  // 订阅者2

每次你调用 collect,会重新执行 flow 内的代码(打印 “Emitting data”)。每个订阅者都重新从头开始收集数据。

1.1 通过冷流实现倒计时功能

fun startCountDownByFlow(
    scope:CoroutineScope,
    delay: Long = 0,
    time:Int = 60, // 倒计时60s
    onTick: (secondUntilFinished: Int) -> Unit,
    onFinish: () -> Unit
):Job{
    return flow {
        delay(delay)
        for (i in time downTo 0) {
            emit(i)
            delay(1000) // 每次延时1s
        }
    }.flowOn(Dispatchers.Main)
        .onCompletion { onFinish() }
        .onEach { onTick.invoke(it) }
        .launchIn(scope)
}

2. 热流(Hot Flow)

定义:热流是一直活跃的流,不依赖订阅者而主动发射数据。

特性:

共享数据源:所有订阅者会共享同一份数据,流的执行不依赖某个具体的订阅者。
没有订阅者时也可能发射数据:即使没有任何订阅者,热流也可以持续发射数据(例如 StateFlow 和 SharedFlow)。
例子:StateFlow 是一个典型的热流:

val stateFlow = MutableStateFlow(0)

// 订阅者1
stateFlow.collect { println(it) }

// 更新状态
stateFlow.value = 1
stateFlow.value = 2
stateFlow 持有最新的数据状态,所有订阅者都能共享并接收该状态的更新。

2.1 onSubscription

onSubscription 是一个在使用 StateFlow 和 SharedFlow 时的回调机制,可以让你在新的订阅者订阅流时执行特定操作。它通常用于执行与流的订阅相关的行为,如初始化、日志记录、或触发某些特定操作。
另外,onSubscription在MutableSharedFlow还有一个巧用,就是可以借助它实现一开始collect就触发数据发送。例如:

	val isRefreshFlow = MutableSharedFlow<Boolean>()
    val isReachEndFlow = MutableSharedFlow<Boolean>()

    val pageState =  flow<Boolean> {
        isRefreshFlow.onSubscription { emit(true)  }.collectLatest {
            isReachEndFlow.collectLatest { ret->
                emit(ret)
            }
        }
    }

在onSubscription里面调用emit,可以实现首次collect的时候有数据发过来。避免内部嵌套的flow无法触发的场景。

3. stateIn 的作用

stateIn 是 Kotlin Coroutines 提供的一个操作符,用于将冷流(Cold Flow)转换成热流(Hot Flow),并将其状态持久化成一个 StateFlow。

如何转换?
当你对一个冷流调用 stateIn 时,它会将这个冷流变成一个热流,即 StateFlow,并开始 立即收集数据,即使没有订阅者。数据会被保存到 StateFlow 中,任何后续订阅者都会接收最新的状态,而不是从头开始重新收集数据。

这个 StateFlow 具有共享特性,所有订阅者都会共享同样的数据状态。并且,这个热流会在你定义的协程作用域中 持续收集数据,直到作用域结束。

stateIn 示例:

val scope = CoroutineScope(Dispatchers.Default)

val coldFlow = flow {
    emit(1)
    emit(2)
    emit(3)
}

// 现在 hotFlow 是一个热流 (StateFlow),持有冷流的数据并共享给多个订阅者
val hotFlow = coldFlow.stateIn(
    scope = scope,
    started = SharingStarted.Lazily,  // 流的启动模式
    initialValue = 0  // 初始值
)

这里的 coldFlow 是一个典型的冷流,只有在调用 collect 时才开始发射数据。而通过 stateIn,我们把它转换成了一个 StateFlow(热流),它会立即开始发射数据,且保存最新的状态,所有订阅者可以共享这个状态。

3.1. stateIn 的参数

  • scope:stateIn 需要一个协程作用域。在这个作用域内,流会被持续收集,作用域结束时流的收集也会停止。

  • started:这是 SharingStarted 的参数,用于控制流何时开始收集数据。

    • SharingStarted.Lazily:只有当有第一个订阅者时才会开始收集数据。
    • SharingStarted.Eagerly:流会立即开始收集,无论是否有订阅者。
    • SharingStarted.WhileSubscribed():只有当有活跃订阅者时才会收集数据,且在没有订阅者后会暂停收集。并且可以指定一个超时时间,例如SharingStarted.WhileSubscribed(5000L),表示5s超时,如果 5 秒内没有新的订阅者,流就会自动停止(即暂停数据收集)。
  • initialValue:指定 StateFlow 的初始值。如果冷流没有任何数据可发射,订阅者会收到这个初始值。

3.2. stateIn 的实际用途

提高性能:对于冷流,每个订阅者都需要重新从头收集数据,可能会导致性能问题。而将冷流转成热流后,数据可以被多次重用,节省计算资源。

确保数据实时性:冷流在没有订阅者时不发射数据,而热流即使没有订阅者也可以持续产生数据。将冷流转换成热流可以确保数据随时可用,特别是在应用中需要长期监听某些数据源时。

共享数据:如果有多个订阅者监听同一个冷流,通过 stateIn 将其转为热流后,多个订阅者可以共享同一个流的数据,而不必每个订阅者都从头开始收集。

总结
stateIn 可以将一个 冷流(Cold Flow) 转换为 热流(Hot Flow)。冷流只有在被订阅时才会开始执行和发射数据,而热流则会持续活跃,并且可以将最新的状态共享给多个订阅者。stateIn 是在需要持久化状态并在多个订阅者之间共享数据时非常有用的工具。并且stateIn之后就变的有状态了,那么就会自带去重复的功能。

4. distinctUntilChanged去重

distinctUntilChanged用于过滤 Flow 中连续重复的元素。具体来说,它的功能是:过滤掉流中连续重复的值,确保只有值发生变化时才会发出新的数据项。源码如下:

public fun <T> Flow<T>.distinctUntilChanged(): Flow<T> =
    when (this) {
        is StateFlow<*> -> this // 如果是 StateFlow,则直接返回
        else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent)
    }

  • distinctUntilChanged 是一个扩展函数,作用于所有 Flow 类型的对象。
  • Flow:这是 Kotlin 中的一种冷流(cold flow),即它只有在被收集时才会启动。
  • StateFlow 特殊处理:StateFlow 本身已经具备去重特性,因此如果流对象是 StateFlow,则直接返回,不再对它应用 distinctUntilChanged。

4.1 使用场景

当 Flow 中观察的对象包含多个属性,且我们只关心某个特定属性的变化时,使用 distinctUntilChanged 或 distinctUntilChangedBy 确实能有效减少不必要的 collect 触发,避免对其他属性的变化做无关处理。
在 Flow 收集(collect)过程中,如果所观察的对象发生了任何变化(即便是不相关的属性),整个对象都会被当作“更新”发射出去,导致 collect 被触发。比如:

data class MyObject(val value1: Int, val value2: Int, val value3: Int)

val flow = MutableStateFlow(MyObject(value1, value2, value3))
// 观察 flow,但实际上只关心 value1 的变化
flow.collect { myObject -> 
    // 每次 flow 更新时都被调用,哪怕 value2 或 value3 改变
}

即使我们只关心 value1 的变化,value2 或 value3 的任何更新也会触发 collect,导致不必要的重新计算、UI 更新等。

使用distinctUntilChanged的好处,可以将数据流限制为仅在我们关注的值(如 value1)发生实际变化时才触发更新。比如:

val flow = MutableStateFlow(MyObject(value1, value2, value3))
val specificValueFlow = flow.map { it.value1 }.distinctUntilChanged()

这段代码会提取 value1 的变化,并在它与上一次相比有差异时才发射,从而减少了 collect 的触发次数。

优势
减少无关数据变化带来的开销:只关注特定属性的变化,不会因为其他属性的改变而触发不必要的处理。
代码清晰:逻辑上更明确,代码仅关注关键属性的变化,更易于维护。

5.关于collect的触发时机

在 StateFlow 或 Flow 中,只有当对象的引用发生了变化(即内存地址不同)时,才会认为是“新”值,从而触发 collect。
在 data class 中,因为默认重写了 equals 和 hashCode 方法,对比的就是data class的所有属性,使得当两个 data class 实例的属性完全相同时,即使它们是不同的实例,StateFlow 也会认为它们是相等的,从而不会触发 collect。在 Kotlin 的 StateFlow 和 SharedFlow 中,判断值是否改变的依据正是 equals 的结果。

在使用 data class 时,由于 equals 和 hashCode 是基于属性自动生成的,所以即使创建了一个新的 MyObject,只要其所有属性值与之前的对象相同,StateFlow 将认为没有变化,因此不会触发 collect。

data class MyObject(val value1: Int, val value2: Int)

val flow = MutableStateFlow(MyObject(1, 2))
flow.collect { println("Collected: $it") }

// 即便创建了新对象,只要属性值相同,就不会触发 collect
flow.value = MyObject(1, 2)  // 不会触发 collect
flow.value = MyObject(2, 2)  // 属性值不同,触发 collect

在实际使用中,如果希望避免重复值触发 collect,data class 是个不错的选择。
但是如果你希望每次修改对象的属性,或者创建新的对象都要触发collect的话,那么就需要使用普通的class对象了。

6. lifecycle.eventFlow

lifecycle.eventFlow 提供了一个与 Lifecycle 事件相关的 Flow,可以用于监听 Lifecycle 状态的变化。它通常用于在 Jetpack Compose 和协程中与生命周期结合,避免传统的生命周期观察器(如 LifecycleObserver)的复杂性。

eventFlow 是 Lifecycle 扩展的一部分,它是一个冷流 (Flow),会在 Lifecycle 事件发生时发射对应的 Lifecycle.Event 值。你可以通过它订阅 Lifecycle 事件,例如 ON_CREATE、ON_RESUME 等。

import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class MyFragment : Fragment(R.layout.fragment_example) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 监听 viewLifecycleOwner 的生命周期事件
        viewLifecycleOwner.lifecycleScope.launch {
            // 保证只有在 STARTED 状态下进行监听
            viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewLifecycleOwner.lifecycle.eventFlow.collect { event ->
                    when (event) {
                        Lifecycle.Event.ON_CREATE -> {
                            // 处理 ON_CREATE 事件
                            println("ON_CREATE triggered")
                        }
                        Lifecycle.Event.ON_START -> {
                            println("ON_START triggered")
                        }
                        Lifecycle.Event.ON_RESUME -> {
                            println("ON_RESUME triggered")
                        }
                        Lifecycle.Event.ON_PAUSE -> {
                            println("ON_PAUSE triggered")
                        }
                        Lifecycle.Event.ON_STOP -> {
                            println("ON_STOP triggered")
                        }
                        Lifecycle.Event.ON_DESTROY -> {
                            println("ON_DESTROY triggered")
                        }
                    }
                }
            }
        }
    }
}

eventFlow 是冷流,只有当你开始收集它时,才会开始监听 Lifecycle 事件。通过 viewLifecycleOwner.lifecycleScope 启动协程,确保它绑定到 viewLifecycleOwner 的生命周期。使用 repeatOnLifecycle 可进一步约束监听的生命周期状态。当 viewLifecycleOwner 的生命周期结束时(如 Fragment 的 onDestroyView 被调用时),eventFlow 的收集会自动取消,避免内存泄漏。

7. combine

在使用多个 Flow 时,combine 是一个非常有用的操作符,可以合并多个 Flow 的最新值并生成新的值流。combine 操作后选择直接 collect 或进一步 stateIn 会影响 Flow 的行为和使用场景。

让我们分别讨论 combine 后直接 collect 和 combine 后再 stateIn 再 collect 的区别,以及它们的典型应用场景。
在这里插入图片描述
如果是一次性监听和处理数据流的结果,使用 combine 后直接 collect,后续只要任意一个 Flow 发送了新值,combine 就会重新组合一次,并通知到 collect 那里,。
如果是需要长期维护合并结果并与多个收集者共享数据,使用 stateIn 将流转换为 StateFlow。

7.1 combine的特点

combine会将两个或多个来源的Flow关联在一起,组成新的结果一起处理并返回,因此一定要注意combine的Flow一定要能保证首次combine的时候能够发射数据(或者有默认值,例如StateFlow),否则combine会一直阻塞着,直到所有的Flow都发射过值才会通知到collect的地方。

  • combine 中“只要任何一个Flow发生变化,就会重新组合并触发 collect”
  • 第一次 collect 开始时,如果两个(或更多)Flow都已经有初始值(比如用的是 StateFlow或者 Flow 本身能马上 emit)就立即组合一次并触发 collect 执行一次
  • 后续任何一个 Flow 只要 emit 新数据,combine 会拿到这个新数据 + 其余的 Flow 当前最新的数据,重新组合一次,触发 collect 执行

7.2 combine和merge的区别

在这里插入图片描述
merge 示例:

val flow1 = flow {
    emit(1)
    delay(100)
    emit(2)
}

val flow2 = flow {
    emit("A")
    delay(50)
    emit("B")
}

// merge
merge(flow1, flow2).collect { value ->
    println(value)
}

输出可能是:

1
A
B
2

谁先 emit 就谁先输出,彼此独立。

combine 示例:

val flow1 = flowOf(1, 2, 3).onEach { delay(100) }
val flow2 = flowOf("A", "B", "C").onEach { delay(200) }

// combine
combine(flow1,flow2) { num, str ->
    "$num -> $str"
}.collect { value ->
    println(value)
}

输出大致是:

1 -> A
2 -> A
2 -> B
3 -> B
3 -> C

要等两边都有值才能组合,生成一个 Pair(或者你定义的新数据)。

  • merge:把多个来源的事件放到一起发,互不关联。比如:多个传感器数据流,谁先来了就先处理。
  • combine:需要两个或多个来源的值关联在一起,组成新的结果一起处理。比如:输入框A、输入框B的内容变化后,才能一起启用提交按钮。

在这里插入图片描述

8.channelFlow

channelFlow 是 Kotlin 协程库中的一个特殊构建器,它结合了 Flow 和协程的优势,提供了一个可用于处理并发任务的流。它是为了能够同时使用 流的收集 和 协程的并发操作 而设计的。它适用于处理同时向流发送多个元素的场景,特别是涉及 并发任务 和 异步操作 时。

在 channelFlow 中,你可以像 Flow 一样收集数据,同时也能利用协程来发送多个值到流。这使得 channelFlow 成为处理高并发、多个任务并行的流处理场景的理想选择。

channelFlow 的关键特点:
支持并发发送多个元素:你可以在 channelFlow 中使用多个协程同时往流中发送元素。
线程安全:channelFlow 内部是基于 Channel 实现的,Channel 提供了线程安全的机制,保证了从多个协程发送数据到流中的正确性。
与 Flow 兼容:它仍然可以像普通的 Flow 一样进行收集、转换和组合等操作。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simpleChannelFlow(): Flow<Int> = channelFlow {
    // 在这里我们可以使用多个协程往这个流中发送数据
    launch {
        for (i in 1..5) {
            delay(100) // 模拟延迟
            send(i) // 向流中发送数据
        }
    }
    // 可以再启动其他的协程发送数据
    launch {
        for (i in 6..10) {
            delay(150) // 模拟不同的延迟
            send(i)
        }
    }

    // 发送完数据后,流会自动关闭
}.flowOn(Dispatchers.IO).stateIn(lifecycleScope, SharingStarted.Lazily, 0)

lifecycleScope.launch {
    simpleChannelFlow().collect { value ->
        println("Received: $value")
    }
}

结果:

 Received: 0
 Received: 1
 Received: 6
 Received: 2
 Received: 7
 Received: 3
 Received: 4
 Received: 8
 Received: 5
 Received: 9
 Received: 10

channelFlow结合热流和冷流的使用:

	private val dataStateFlow = MutableStateFlow<String?>(null)

    private val dataFlow = flow<String> {
        for(i in 0..10){
            emit(i.toString())
            delay(1000)
        }
    }

    private val dataChannelFlow = channelFlow<String> {
       launch{
           dataFlow.collectLatest {
               send(it)
           }
       }

       launch {
           dataStateFlow.collectLatest {
               if (it != null) {
                   send(it)
               }
           }
       }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val scrollState = rememberScrollState()
            Column (
                modifier = Modifier
                    .windowInsetsPadding(WindowInsets.systemBars)
                    .background(Color.White)
                    .fillMaxSize()
                    .verticalScroll(scrollState)
            ) {
                val data = dataChannelFlow.collectAsState(initial = "")

                Button(onClick = {
                    dataStateFlow.value = UUID.randomUUID().toString()
                }) {
                    Text(text = "test")
                }
                Text(text= data.value)
            }
        }
    }

效果图:
在这里插入图片描述

  • dataFlow 是冷流,在 collect 触发时才会开始发射数据。
  • dataStateFlow 是热流,每当值改变时,都会发射新值。
  • dataChannelFlow 同时收集 dataFlow 和 dataStateFlow,并将两者的值发送给 UI。
  • 无论是 dataFlow 还是 dataStateFlow 发生变更,都会导致 dataChannelFlow 的值变更,并触发 UI 的刷新。

dataChannelFlow首次collectAsState会触发dataFlow和dataChannelFlow的collectLatest,所以会看到一开始页面会出现0~10的数字变化,每次鼠标点击按钮的时候,会修改dataStateFlow的值,导致dataChannelFlow再次变更,然后刷新UI。

9. channel

Channel 是 Kotlin 协程(Kotlin Coroutines) 提供的一种用于协程之间通信的工具,类似于其他语言中的“消息队列”或者“管道(Pipe)”。
Channel 是一个并发安全的通信工具,用于在多个协程之间传递数据。
它的核心用途:
在 一个协程中发送数据(send())
在 另一个协程中接收数据(receive())

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val channel = Channel<Int>()

    // 启动一个发送数据的协程
    launch {
        for (i in 1..5) {
            channel.send(i) // 发送数据
            println("Sent $i")
        }
        channel.close() // 关闭通道,表示不再发送数据
    }

    // 在当前协程中接收数据
    for (x in channel) {
        println("Received $x")
    }
}

输出log

Sent 1
Received 1
Sent 2
Received 2
...

通道类型和构造函数

  • Channel():默认是无缓冲的(发送会挂起直到被接收)
  • Channel(capacity):设置缓冲区容量

工厂方法:

  • Channel.UNLIMITED:无限缓冲区

  • Channel.CONFLATED:只保留最新的值

  • Channel.RENDEZVOUS:无缓冲,默认

  • Channel.BUFFERED:自动选择一个合理的缓冲大小

Channel vs Flow

特性ChannelFlow
数据生产者主动发送(send被动收集(collect
多发送者/接收者支持默认是单向、单收集器
关闭显式关闭(close()自动完成
用途通信、事件推送、消息流数据流、变换、响应式编程

9.1. channel 转flow

这是 kotlinx.coroutines.channels 包中定义的扩展函数,它会将 Channel 转换成一个 Flow,你就可以像处理 Flow 那样用 collect() 来消费数据。
channel.consumeAsFlow() 和 channel.receiveAsFlow() 是将 Channel 转换为 Flow 的两种方法,但它们在行为、用途和生命周期管理上有显著区别

channel.consumeAsFlow()

定义:consumeAsFlow() 将一个 Channel 转换为 Flow,并在收集 Flow 时消费 Channel 中的所有元素。每次收集都会从 Channel 中读取数据,直到 Channel 关闭。
行为:

消费性:consumeAsFlow() 会独占 Channel,收集 Flow 时会直接消耗 Channel 中的元素,导致其他消费者无法再从同一 Channel 接收数据。
关闭 Channel:当 Flow 的收集被取消(如协程取消或作用域结束),consumeAsFlow() 会自动关闭底层 Channel。

单消费者:适合只有一个消费者需要读取 Channel 数据的场景。
实现细节:

调用后,Channel 被绑定到该 Flow,数据会被 Flow 独占读取。
收集结束(或取消)后,Channel 会被关闭,释放资源。

代码示例:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (i in 1..3) {
            channel.send(i)
            delay(100)
        }
        channel.close()
    }

    val flow = channel.consumeAsFlow()
    flow.collect { value ->
        println("Received: $value")
    }
}

输出结果:

Received: 1
Received: 2
Received: 3

注意事项:
如果尝试对同一个 Channel 多次调用 consumeAsFlow() 或其他消费者读取,会抛出异常或导致未定义行为,因为 Channel 已被独占。
适合需要将 Channel 数据转为 Flow 且只需要单一消费者的场景。

channel.receiveAsFlow()

定义:receiveAsFlow() 也将 Channel 转换为 Flow,但它允许 Channel 继续被其他消费者使用,不会独占 Channel。

行为:
非消费性:receiveAsFlow() 不会消耗 Channel 的数据,Channel 可以继续被其他消费者(如其他 Flow 或直接 receive())读取。
不自动关闭 Channel:Flow 的收集取消不会关闭底层 Channel,Channel 生命周期由其他机制控制(如手动关闭或发送方关闭)。

多消费者:适合多个消费者需要从同一 Channel 读取数据的场景(如广播)。

实现细节:
每次 receiveAsFlow() 创建一个新的 Flow,基于 Channel 的 receive() 操作,数据可以被多个 Flow 或其他消费者共享。
Flow 仅反映 Channel 的接收操作,不会影响 Channel 本身。

代码示例:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val channel = Channel<Int>()
    launch {
        for (i in 1..3) {
            channel.send(i)
            delay(100)
        }
        channel.close()
    }

    val flow1 = channel.receiveAsFlow()
    val flow2 = channel.receiveAsFlow()

    launch {
        flow1.collect { println("Flow1: $it") }
    }
    launch {
        flow2.collect { println("Flow2: $it") }
    }
}

输出(可能交错):

Flow1: 1
Flow2: 1
Flow1: 2
Flow2: 2
Flow1: 3
Flow2: 3

两个 Flow 都可以接收到 Channel 的数据,因为 receiveAsFlow() 不独占 Channel。

注意事项:
数据是竞争性消费的,多个消费者会瓜分 Channel 中的元素(例如,Flow1 可能收到 1、3,Flow2 收到 2)。
如果 Channel 是 BroadcastChannel 或 Con4uentChannel,可以用 asFlow() 替代(Kotlin 1.7.0+ 中已废弃,推荐使用 SharedFlow)。
需手动管理 Channel 的关闭,否则可能导致资源泄漏。

consumeAsFlow() vs receiveAsFlow()

特性consumeAsFlow()receiveAsFlow()
消费性独占 Channel,消费所有元素非独占,允许多个消费者共享
Channel 关闭不会自动关闭 Channel,需手动关闭Flow 取消不影响 Channel
适用消费者数量单一消费者多个消费者
数据分发所有数据给单一 Flow所有订阅者都收到相同数据(广播式)
典型场景单点消费(如单个通知更新)多点消费(如广播事件给多个观察者)
资源管理Flow 会完成,但 Channel 需手动关闭Flow 与 Channel 生命周期解耦,需自行管理

9.2. flow转channel

在 Kotlin 协程中,如果你想把一个 Flow 转换为 Channel,可以通过使用 produceIn(scope) 方法,它会在给定的 CoroutineScope 中启动一个协程,并将 Flow 的数据发送到返回的 ReceiveChannel 中。

val flow = flow {
    emit("Hello")
    delay(1000)
    emit("World")
}

val channel: ReceiveChannel<String> = flow.produceIn(scope)

scope.launch {
    for (value in channel) {
        println("Received: $value")
    }
}

for (value in channel) 等同于channel.receive() ,表示从 Channel 接收数据。

⚠️ 注意事项:
produceIn(scope) 会立即开始收集 Flow。
返回的是 ReceiveChannel,不能发送数据(如果你需要发送 + 接收,请考虑 Channel + launch 自行连接)。

类型方向说明
Channel<T> 实现类双向可发送 send() 也可接收 receive()
SendChannel<T> 接口只写只能 send(),不能 receive()
ReceiveChannel<T> 接口只读只能 receive(),不能 send()

Kotlin 中的 Channel 同时实现了 SendChannel 和 ReceiveChannel 接口。

9.3. select函数

select 表达式可以同时监听多个 ReceiveChannel,哪个通道先收到数据就优先处理哪个,也叫做“只挑一个先响应的”机制,一旦某个 ReceiveChannel 收到数据并被处理,整个 select {} 表达式就会立即返回结果。
例如:

val ch1 = Channel<String>()
val ch2 = Channel<String>()

val result = select<String> {
    ch1.onReceive { value ->
        "ch1 says: $value"
    }
    ch2.onReceive { value ->
        "ch2 says: $value"
    }
    onTimeout(500L) {
        "Timeout!"
    }
}

结果:

  • 如果 ch1 先收到数据,返回 “ch1 says: …”

  • 如果 ch2 先收到数据,返回 “ch2 says: …”

  • 如果都没数据,500ms 后返回 “Timeout!”

✅ select 支持的挂起方法汇总:

类型扩展函数说明
JobonJoin当 Job 完成时触发(即 job.join() 结束)
Deferred<T>onAwait当异步任务返回结果时触发(即 await() 完成)
ReceiveChannel<T>onReceive有数据可读时触发(读取一个值)
ReceiveChannel<T>onReceiveCatching更安全的接收(可处理关闭状态,避免异常)
SendChannel<T>onSend(value)当可以发送指定值时触发
内部辅助onTimeout(timeMs)超时触发(非挂起源,但非常常用)

📝 补充说明:

  • onReceiveCatching 优于 onReceive,因为它可以优雅地处理通道关闭。

  • onSend(value) 是用于尝试发送数据,而不是接收。

  • onJoin、onAwait 用于同步等待任务或异步值的完成。

  • onTimeout 是特有于 select 的超时挂起模拟机制。

示例:

val ch1 = Channel<String>()
val deferred = async { delay(500); "Done" }
val job = launch { delay(1000) }

val result = select<String> {
    ch1.onReceive { "Received: $it" }

    deferred.onAwait { "Deferred result: $it" }

    job.onJoin { "Job completed" }

    onTimeout(300L) { "Timeout occurred" }
}

println(result)

select 表达式中的 onXxx 方法本质上就是对应 协程中的挂起函数(suspending functions) 的一种 “选择监听版本”。它们之间存在明显的一一对应关系,区别只在于:

xxx() 是在协程中挂起等待某件事完成;

onXxx { … } 是在 select 中注册对这件事的监听,如果满足条件就立即执行对应的 block。

✅ 对应关系表:

协程挂起函数select 中的监听函数含义说明
job.join()job.onJoin { ... }等待协程结束
deferred.await()deferred.onAwait { ... }等待异步结果
channel.receive()channel.onReceive { ... }从通道接收数据
channel.send(x)channel.onSend(x) { ... }向通道发送数据
delay(time)onTimeout(time) { ... }实现超时等待(注意:不是 delay 的真实对应)

select 表达式本身 只会挂起一次并返回一个结果,执行完毕后就不会再继续监听其他分支。因此非常适合处理多个异步来源,等待其中之一完成并立即响应。

🔄 如果你想“持续监听”多个通道怎么办?
你需要把 select 放在一个循环里,例如:

while (isActive) {
    select<Unit> {
        channel1.onReceive { value ->
            println("Got $value from channel1")
        }
        channel2.onReceive { value ->
            println("Got $value from channel2")
        }
    }
}

这种模式就能持续监听多个 channel,每次 select 等待一个结果,返回后再下一轮继续。
另外,在协程结束后,你应该手动关闭 channel1 和 channel2,以避免资源泄露或内存占用,尤其是在你自己创建的通道中(如 Channel() 而非外部库返回的只读 channel)。

val channel1 = Channel<Int>()
val channel2 = Channel<Int>()

val job = launch {
    while (isActive) {
        select<Unit> {
            channel1.onReceive {
                println("Received from channel1: $it")
            }
            channel2.onReceive {
                println("Received from channel2: $it")
            }
        }
    }
}

// 假设运行一段时间后取消
delay(5000)
job.cancelAndJoin()

// ✅ 主动关闭通道,释放资源
channel1.close()
channel2.close()

⚠️ 注意:

  • 关闭通道只能由“发送方”来做,接收方不要主动关闭它。

  • 通道关闭后,再发送会抛出 ClosedSendChannelException。

  • 接收关闭通道不会出错,只是会接收到 Closed 或空数据(使用 onReceiveCatching { … } 可判断 it.isClosed)。

例如:

// 将ShareFlow转成ReceiveChannel
val closeAppChannel = GlobalEventManager.onServiceDestroyShareFlow
    .filter { it } // 只关心为 true 的事件
    .produceIn(this) // 将 Flow 转为 ReceiveChannel

val result = try {
    select<SendFileMsgState> {
         copyDeferred.onAwait { copyResult -> ... }
   		 closeAppChannel.onReceiveCatching { result -> ... }
    }
} finally {
    closeAppChannel.cancel() // ❗资源释放
}

10. callbackFlow

callbackFlow 是一个桥梁,把传统的基于回调的异步代码(如监听器、事件回调)封装进 Flow,让你可以用 collect() 来消费这些事件。
常用于下列场景:

  • 把监听器接口(如 Android 中的 LocationListener、TextWatcher)封装成 Flow
  • 把回调式网络/异步操作 封装成 Flow
  • 把事件总线、广播 转换为流式数据源
fun locationUpdates(): Flow<Location> = callbackFlow {
    val listener = object : LocationListener {
        override fun onLocationChanged(location: Location) {
            trySend(location) // 向 Flow 发出一个值
        }
    }

    locationManager.requestLocationUpdates(listener)

    // 当 Flow 被取消或完成时执行
    awaitClose {
        locationManager.removeUpdates(listener)
    }
}

关键点说明

元素说明
callbackFlow { ... }构建一个 Flow,内部可以用 trySend() 发送数据
trySend(value)非阻塞地向 Flow 发送值(如果缓冲区满,返回失败)
awaitClose { ... }用于清理资源(比如注销监听器),当 Flow 被取消时调用
默认是冷流callbackFlow 创建的 Flow 是懒执行的,只有被 collect 时才启动
带缓冲区的 ChannelcallbackFlow 底层使用的是 Channel,可以配置容量

注意事项

  • callbackFlow 必须调用 awaitClose(),否则资源不会释放
  • 它是 channelFlow 的子集(只能从一个线程发送)
  • 使用 trySend() 而不是 send(),避免挂起(推荐方式)

简单示例封装:封装按钮点击事件

fun View.clicks(): Flow<Unit> = callbackFlow {
    val listener = View.OnClickListener {
        trySend(Unit)
    }
    setOnClickListener(listener)
    awaitClose { setOnClickListener(null) }
}

与 flow {} 的区别

特性flow {}callbackFlow {}
异步事件流❌ 不擅长✅ 非常擅长
回调封装❌ 不支持✅ 支持
内部多线程支持❌ 仅限顺序调用✅ 支持多线程调用(推荐用 trySend()
清理资源❌ 需外部处理awaitClose() 提供释放机制

11. snapshotFlow

snapshotFlow 只能用于 Jetpack Compose 环境中,因为它是专门为 监听 Compose 状态快照(Snapshot)系统 而设计的.

val pagerState = rememberPagerState(initialPage = initialSelectedIndex, pageCount = { items.size })
LaunchedEffect(pagerState) {
    snapshotFlow { pagerState.isScrollInProgress }
        .collect { inProgress ->
            userScrollStarted.value = inProgress
        }
}

snapshotFlow 示例(监听 Compose 状态)

val count = remember { mutableStateOf(0) }

LaunchedEffect(Unit) {
    snapshotFlow { count.value }
        .collect { value ->
            Log.d("Flow", "Count changed to $value")
        }
}

这个 Flow 会 自动在 count.value 变化时发射新值。
什么时候用 snapshotFlow?

场景是否适合用 snapshotFlow
监听 ScrollState.valuePagerState.currentPage✅ 是
监听 mutableStateOf 状态变化✅ 是
发出定时器/轮询数据❌ 否,使用普通 flow
网络请求、数据库变更❌ 否,使用普通 flowcallbackFlow
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值