我们来系统学习一下 Flow 的终端操作符。
Flow 是冷流,只有在调用了终端操作符之后,才会启动流的收集流程(collect),上游的代码才会开始执行。
获取单个元素
这类操作符用于从流中获取特定的值。
注意:它们都是挂起函数。
first() 和 firstOrNull()
first():它会等待并返回 Flow 发射的第一个 元素,然后立即取消流的收集。
kotlin
fun main(): Unit = runBlocking {
// 获取第一个值
val flow = flowOf(1, 2, 3)
println("First value: ${flow.first()}") // 输出: 1
// 异常:流为空
val emptyFlow = flowOf<Int>()
try {
emptyFlow.first()
} catch (e: NoSuchElementException) {
println("Error: ${e.message}") // 输出: Expected at least one element
}
}
它的变体是 first{ predicate },会返回第一个满足条件的元素。
kotlin
val flow = flowOf(1, 2, 3)
// 查找第一个大于等于 2 的元素
val value = flow.first { it >= 2 }
firstOrNull() 的行为和 first() 类似,区别在于当 Flow 为空时,它不会抛出异常,而是会返回 null。
使用场景
对于不需要持续监听变化的 Flow,我们会经常使用此操作符。
例如:读取 DataStore 的当前快照、将一次性的网络请求封装为 Flow。
读取完毕后,流就会自动关闭,不会造成资源泄漏。
last() 和 lastOrNull()
这两个操作符必须等待流完全结束后,才会返回流的最后一个元素。
千万注意 :如果在无限流 上调用 last(),会导致协程永远挂起,进而引出内存泄漏或界面无响应。
无限流场景:
SharedFlow- 通过
interval创建的时间流 - Room 数据库的查询 Flow,只要数据表变动,它就会发射新数据,所以它永远不会主动结束。因此,不要在 Dao 返回的 Flow 上调用
last()。
single() 与 singleOrNull()
这类操作符不仅仅是获取值,还有检验的性质。
single():期待流仅仅发射一个元素。
- 如果流只发射了一个元素,会返回该元素。
- 如果流为空,会抛出
NoSuchElementException。 - 如果流的发射了多于一个元素,会抛出
IllegalStateException。
singleOrNull():同上,区别在于当流为空或是元素多于一个时,会返回 null。
注意:使用 singleOrNull(),你将无法区分"没有数据"或是"数据超出一个"的情况。如果要区分数据类型,应该使用 single()。
集合转换与统计
这类操作符会收集流中的所有数据,需要等待流的结束。所以它也同样存在无限挂起的风险。
count()
作用:获取流中元素的总个数。
变体:count { predicate } 会统计满足条件的元素个数。
toList(), toSet(), toCollection()
toList():将所有元素收集到List中。toSet():将所有元素收集到Set中。toCollection(destination):将所有元素添加到指定的集合实例中。
注意:Flow 的优势是流式处理,使用这些操作符会将所有数据一次性加载到内存中。如果数据量很大,极易导致 OOM。
流与通道的桥梁
produceIn(scope) 并不是一个挂起函数,而是一个普通函数。
它可以将一个 Flow(冷流)转为 ReceiveChannel(热通道)。
原理:其实在内部它只是利用传入的 CoroutineScope 启动一个新的协程来收集这个 Flow,并将数据发送到 Channel 中。
在 MVVM 架构中,将冷流转为热流暴露给 UI,我们通常会使用 stateIn 或 shareIn。但在需要利用 Channel 特有的并发能力时,produceIn 还是首选的工具。