引言
在现代异步编程中,处理数据序列(尤其是来自异步源的数据序列,如网络响应、数据库查询、用户事件流等)是一个核心需求。Kotlin Coroutines 为我们提供了强大的底层并发原语,而 Flow 则是在 Coroutines 基础上构建的一套用于处理异步数据流的声明式、响应式 API。它设计简洁、可组合性强,并且与 Kotlin 的协程深度集成,是替代 RxJava 或 LiveData(在特定场景)的更 Kotlin 原生、更轻量级的选择。
1. Flow 的核心概念 (The Essence)
-
什么是 Flow?
- Flow 代表一个异步计算的数据序列。
- 它是 "冷流"(Cold Stream) :Flow 的构建器(如
flow { ... })定义了一个数据序列的生产过程,但这个生产过程只会在收集器(collect)开始收集数据时才会启动 ,并且每次收集都会启动一个新的独立执行过程(类似于Sequence)。这与"热流"(Hot Stream)不同,热流的生产过程独立于收集者存在(如BroadcastChannel或SharedFlow/StateFlow)。 - 背压(Backpressure)友好:Flow 天然支持背压。收集者的处理速度决定了生产者的发射速度。如果收集者处理不过来,生产者会暂停发射,直到收集者准备好接收新数据。这是通过协程的挂起机制实现的。
- 结构化并发(Structured Concurrency) :Flow 的生命周期紧密绑定到收集它的协程作用域(
CoroutineScope)。当收集协程被取消时,Flow 的生产过程也会被自动取消,资源得到妥善释放。
-
关键组件:
- 生产者(Producer) :负责产生数据。使用
flow { ... }构建器、flowOf(...)或 扩展函数(如.asFlow())创建。 - 中间操作符(Intermediate Operators) :在数据从生产者流向消费者的过程中,对数据流进行转换、过滤、组合等操作(如
map,filter,transform,zip,combine)。这些操作符通常是挂起函数 或返回新 Flow 的函数,它们本身不触发数据生产。 - 消费者(Consumer / Collector) :最终接收并处理数据。主要使用
collect { ... }终端操作符来触发整个流的执行并消费数据。其他终端操作符如first(),toList(),count(),fold(),launchIn()等也会触发执行。
- 生产者(Producer) :负责产生数据。使用
2. 基础用法 (The Basics)
-
创建 Flow:
flow { ... }构建器:最常用。内部使用emit(value)函数发送值,可以调用其他挂起函数。
kotlinfun simpleFlow(): Flow<Int> = flow { println("Flow started") for (i in 1..3) { delay(100) // 模拟异步工作 emit(i) // 发送值 } }flowOf(...):快速创建一个发射固定值的 Flow。
kotlinval numberFlow: Flow<Int> = flowOf(1, 2, 3, 4).asFlow()扩展函数:将集合、范围、序列等转换为 Flow。
kotlin(1..5).asFlow() listOf("a", "b", "c").asFlow() -
收集 Flow (触发执行):
- 使用
collect终端操作符。
kotlinrunBlocking { simpleFlow().collect { value -> // 开始收集,触发生产者执行 println("Collected $value") } } // 输出: // Flow started // Collected 1 (延迟约100ms) // Collected 2 (延迟约100ms) // Collected 3 (延迟约100ms)注意
runBlocking仅用于示例,在主线程阻塞等待。实际应用中应在合适的协程作用域(如viewModelScope,lifecycleScope)内收集。 - 使用
-
常用中间操作符:
map: 转换每个元素。filter: 过滤元素。transform: 更灵活的转换,可以发射任意次(包括零次)任意值,甚至改变流类型。take: 只取前 n 个元素。onEach: 在每次发射前执行一个动作(不改变元素)。catch: 捕获上游操作符中的异常。
kotlinrunBlocking { (1..5).asFlow() .filter { it % 2 == 0 } // 只取偶数 .map { it * it } // 平方 .onEach { println("Processing $it") } // 处理前打印 .collect { println("Result: $it") } } // 输出: // Processing 4 // Result: 4 // Processing 16 // Result: 16
3. 流上下文与异常处理 (Context & Exception Handling)
-
流的上下文继承 (Context Preservation):
- 默认情况下,Flow 的生产者代码(
flow { ... }内部)会继承 调用collect的协程的CoroutineContext。 - 这通常意味着生产者和消费者在同一个线程/调度器上运行。如果生产者是 CPU 密集型或阻塞 IO,可能会阻塞消费者线程。
- 默认情况下,Flow 的生产者代码(
-
flowOn操作符:- 用于改变上游操作符(在它之前的操作符)的执行上下文。
- 它创建一个新的协程在指定的
CoroutineContext(通常是Dispatchers)中运行上游代码。 - 下游操作符(包括
collect)的上下文不受影响。
kotlinfun simpleFlow(): Flow<Int> = flow { println("Emitting on ${Thread.currentThread().name}") // 在 IO 线程 for (i in 1..3) { delay(100) emit(i) } }.flowOn(Dispatchers.IO) // 改变上游(flow构建器)的上下文 runBlocking { simpleFlow() .map { // 这个 map 在收集器的上下文运行 (DefaultDispatcher-worker-1 或 main) println("Mapping $it on ${Thread.currentThread().name}") it * it } .collect { // 也在收集器的上下文 println("Collecting $it on ${Thread.currentThread().name}") } } // 输出示例: // Emitting on DefaultDispatcher-worker-1 // Mapping 1 on main @coroutine#1 // Collecting 1 on main @coroutine#1 // ... (注意:生产在IO线程,消费转换在主线程) -
异常处理:
catch操作符 :用于捕获上游操作符中发生的异常。它允许你处理异常、发出错误值、或重新抛出。try/catch块 :可以包裹collect或其他终端操作符来捕获消费者端的异常。- 声明式处理 :
catch是处理生产者异常的首选声明式方式。
kotlinfun faultyFlow(): Flow<String> = flow { emit("One") throw RuntimeException("Oops!") emit("Two") // 不会执行 } runBlocking { faultyFlow() .catch { e -> // 捕获上游异常 println("Caught exception: $e") emit("Fallback") // 发射一个回退值 } .collect { value -> println(value) } } // 输出: // One // Caught exception: java.lang.RuntimeException: Oops! // Fallback
4. 高级操作符与组合 (Advanced Operators & Composition)
-
zip:- 将两个 Flow 的对应位置的元素组合成一个新元素(通过提供的转换函数)。它等待两个 Flow 都发射了对应的元素后才组合并向下游发射。
- 如果两个 Flow 长度不同,结果 Flow 的长度等于较短的那个 Flow 的长度。
kotlinval nums = (1..3).asFlow().onEach { delay(300) } val strs = flowOf("One", "Two", "Three").onEach { delay(400) } runBlocking { nums.zip(strs) { a, b -> "$a -> $b" } .collect { println(it) } } // 输出 (间隔约400ms): // 1 -> One // 2 -> Two // 3 -> Three -
combine:- 每当任意一个 Flow 发射新值时,就将两个 Flow 最新的元素组合成一个新元素(通过提供的转换函数)发射出去。
- 结果 Flow 发射的频率取决于两个源 Flow 中发射更频繁的那个。
- 非常适合需要根据多个流最新状态更新 UI 或进行计算的场景。
kotlinval nums = (1..3).asFlow().onEach { delay(300) } // 300ms, 300ms, 300ms val strs = flowOf("A", "B", "C").onEach { delay(400) } // 400ms, 400ms, 400ms runBlocking { nums.combine(strs) { a, b -> "$a$b" } .collect { println(it) } } // 可能的输出顺序: // 1A (在300ms,strs 第一个还没发,但 combine 要求每个流至少有一个值才开始?实际上combine会在两个流都有初始值后开始) // 2A (在600ms: nums 发2, strs 最新还是A) // 2B (在800ms: strs 发B, nums 最新是2) // 3B (在900ms: nums 发3, strs 最新是B) // 3C (在1200ms: strs 发C, nums 最新是3)(图示:combine vs zip 的发射时机差异)
luanums: ----1----2----3| strs: --------A--------B--------C| zip: --------(1,A)--------(2,B)--------(3,C)| combine: ----1A--2A--2B--3B--3C| (时间线简化示意) -
flatMapConcat,flatMapMerge,flatMapLatest:- 用于处理"Flow of Flows"的场景。它们接收一个 Flow(
outerFlow)和一个转换函数(该函数将outerFlow的每个元素转换为一个新的 FlowinnerFlow),然后将这些innerFlow"展平"(flatten) 成单个结果 Flow。区别在于如何订阅和处理内部的innerFlow:flatMapConcat: 顺序 执行。等待当前的innerFlow完全完成 后,才开始收集下一个innerFlow。结果流中的元素顺序严格对应outerFlow的元素顺序。flatMapMerge: 并发 执行。同时收集所有已发射的innerFlow(并发数量可通过concurrency参数限制)。结果流中的元素顺序是任意的,取决于各个innerFlow的完成速度。flatMapLatest: 最新优先 。每当outerFlow发射一个新元素时,它就会取消 掉前一个仍在运行的innerFlow的收集,并立即开始收集新的innerFlow。只关心最新的值。
kotlinfun requestFlow(i: Int): Flow<String> = flow { emit("$i: First") delay(500) emit("$i: Second") } runBlocking { // flatMapConcat (1..3).asFlow().onEach { delay(100) } .flatMapConcat { requestFlow(it) } .collect { println("Concat: $it") } // flatMapMerge (concurrency = 2) (1..3).asFlow().onEach { delay(100) } .flatMapMerge(2) { requestFlow(it) } .collect { println("Merge: $it") } // flatMapLatest (1..3).asFlow().onEach { delay(100) } .flatMapLatest { requestFlow(it) } .collect { println("Latest: $it") } } // flatMapConcat 输出 (顺序): // Concat: 1: First // Concat: 1: Second // Concat: 2: First // Concat: 2: Second // Concat: 3: First // Concat: 3: Second // flatMapMerge 输出 (可能顺序, 1和2并发): // Merge: 1: First // Merge: 2: First // Merge: 1: Second // Merge: 2: Second // Merge: 3: First // Merge: 3: Second // flatMapLatest 输出 (当2发射时取消1的第二个值,当3发射时取消2的第二个值): // Latest: 1: First // (100ms后2发射,取消1) // Latest: 2: First // (100ms后3发射,取消2) // Latest: 3: First // Latest: 3: Second - 用于处理"Flow of Flows"的场景。它们接收一个 Flow(
5. 状态容器:SharedFlow 与 StateFlow (State Holders)
Flow 默认是冷的,每个收集器获得独立的流执行。但在许多场景(如状态管理、事件广播),我们需要热的、可共享的数据流:
-
SharedFlow:- 一个热流。数据生产独立于收集者的存在。即使没有收集者,生产者也可以发射数据(可配置缓存)。
- 多个收集者共享同一个数据流 。新的收集者默认只能收到订阅之后 发射的数据(除非配置了
replay)。 - 关键配置参数:
replay: 缓存多少条历史数据给新的收集者。replay = 0表示新收集者只收新数据。extraBufferCapacity: 在已有缓存 (replay) 之外,还能额外缓存多少条数据(当收集者跟不上时)。onBufferOverflow: 当缓存区满时的策略(BufferOverflow.SUSPEND挂起生产者(默认),DROP_OLDEST丢弃最老,DROP_LATEST丢弃最新)。
- 创建: 使用
MutableSharedFlow作为可写的共享流,并通过.asSharedFlow()将其暴露为只读的SharedFlow给消费者。
kotlinclass EventBus { // 私有可写实例,配置replay=0, 无额外缓存,溢出挂起 private val _events = MutableSharedFlow<Event>() // 公开只读视图 val events: SharedFlow<Event> = _events.asSharedFlow() suspend fun postEvent(event: Event) { _events.emit(event) // 挂起直到事件被分发或缓存 } } // 使用 val eventBus = EventBus() // 收集者1 viewModelScope.launch { eventBus.events.collect { event -> println("Collector1: $event") } } // 收集者2 (稍后订阅) viewModelScope.launch { delay(1000) eventBus.events.collect { event -> println("Collector2: $event") } } // 发送事件 viewModelScope.launch { eventBus.postEvent(Event("A")) // 只有Collector1收到 delay(500) eventBus.postEvent(Event("B")) // 只有Collector1收到 delay(1000) // 此时Collector2已订阅 eventBus.postEvent(Event("C")) // Collector1和Collector2都收到 } -
StateFlow:StateFlow是SharedFlow的一个特殊化、高度配置 的版本,专为表示可观察的状态而设计。- 必须有一个初始值。
replay = 1且固定 :新的收集者会立即 收到当前的最新状态值(即那个replay=1的缓存值)。- 值相等性 (
distinctUntilChanged()) :通过Any.equals比较新值和当前值。只有新值与当前值不同时,才会更新状态并通知收集者。这避免了不必要的 UI 更新或计算。 - 创建: 使用
MutableStateFlow(initialValue)创建可写实例,并通过.asStateFlow()暴露为只读的StateFlow。
kotlinclass CounterViewModel : ViewModel() { // 私有可写状态 private val _count = MutableStateFlow(0) // 初始值0 // 公开只读状态 val count: StateFlow<Int> = _count.asStateFlow() fun increment() { _count.value++ // 更新状态 (会检查distinctUntilChanged) } } // UI (Compose 示例) @Composable fun CounterScreen(viewModel: CounterViewModel = viewModel()) { val count by viewModel.count.collectAsStateWithLifecycle() // 收集状态流,感知生命周期 Column { Text(text = "Count: $count") Button(onClick = { viewModel.increment() }) { Text("Increment") } } } -
SharedFlow vs StateFlow:
特性 SharedFlowStateFlow初始值 无 必须有 新订阅者获取历史 通过 replay配置 (可0, 1, n)总是获取最新一个值 ( replay=1固定)值更新条件 无条件发射 (除非缓存满策略) 仅当新值 !=当前值 (distinctUntilChanged)典型用途 事件总线 (单次事件) 状态容器 (可观察状态) 是否热流 是 是 背压处理 通过配置 ( extraBufferCapacity,onBufferOverflow)固定配置 (类似 replay=1,onBufferOverflow=SUSPEND)(图示:StateFlow 的初始值和 distinctUntilChanged)
iniInitial: State = 0 Collector1 subscribes -> immediately gets 0 Update: State = 1 (different) -> Collector1 notified Update: State = 1 (same) -> NO notification Collector2 subscribes later -> immediately gets current State (1)
6. Flow 与 Channel (Flow vs Channels)
Channel:是 Kotlin 协程中用于协程间通信 的原语 。它提供了一种在不同协程之间安全传递元素的方式(生产-消费模式)。Channel可以有多个发送者 (send) 和多个接收者 (receive)。Channel本身更底层。Flow:建立在协程(可能用到Channel内部实现某些操作符如buffer)之上的声明式、响应式数据流 API 。它专注于数据的转换和处理管道 ,通过collect消费。Flow 通过挂起机制天然处理背压。- 关系与选择:
Flow更适合构建数据处理管道和响应式流。它是处理异步数据序列的首选高级 API。- 当你需要非挂起的发送(
trySend) 、或者需要更精细地控制元素在协程间的传递(如 rendezvous 行为)时,可能需要直接使用Channel。 SharedFlow和StateFlow在内部使用Channel实现,但它们提供了更高级别的抽象。- 建议 :优先使用
Flow,特别是SharedFlow/StateFlow满足状态/事件需求。仅在FlowAPI 无法直接满足底层通信原语需求时才考虑直接使用Channel。
7. 实战示例:网络请求与数据库 (Practical Example: Network + DB)
一个常见模式:从网络加载数据,缓存到数据库,UI 观察数据库中的缓存数据。
kotlin
// 假设有 Room Dao
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Query("SELECT * FROM user WHERE id = :userId")
fun getUser(userId: String): Flow<User?> // Room 支持返回 Flow!
}
// Repository
class UserRepository(private val userDao: UserDao, private val api: UserApi) {
// 获取用户数据流。优先从DB读取,并尝试刷新网络数据。
fun getUser(userId: String): Flow<User?> = flow {
// 1. 先发射本地数据库中的值 (可能是null或旧数据)
userDao.getUser(userId).collect { emit(it) }
// 2. 尝试从网络获取最新数据
try {
val freshUser = api.fetchUser(userId) // 挂起网络请求
userDao.insert(freshUser) // 更新数据库
// Room的Flow会自动再次发射新值给上面的collect,所以这里不需要再emit
} catch (e: Exception) {
// 处理网络错误 (可选: 发射错误事件流? 或静默失败,依赖本地数据)
println("Network error: ${e.message}")
}
}
}
// ViewModel
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val userId = MutableStateFlow("defaultId")
// 暴露给UI的用户状态流,基于userId变化自动切换观察对象
val user: StateFlow<User?> = userId.flatMapLatest { id -> // 当userId变化时,取消旧请求,开始新请求
repo.getUser(id).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 配置共享策略 (5s超时停止)
initialValue = null
)
}.stateIn( // 将最终的Flow转换为StateFlow给UI
scope = viewModelScope,
started = SharingStarted.Eagerly, // 或 WhileSubscribed
initialValue = null
)
fun loadUser(newId: String) {
userId.value = newId
}
}
关键点解释:
userDao.getUser(userId)返回一个Flow<User?>。当数据库中的用户数据发生变化时(比如我们insert了新数据),Room 会自动通知这个 Flow 并发射最新的值。repo.getUser方法:- 首先通过
userDao.getUser(userId).collect { emit(it) }立即发射数据库中的当前值(可能为null)。 - 然后尝试进行网络请求。
- 如果网络请求成功,将新数据
insert到数据库。 - 由于 Room Flow 的自动通知特性 ,当数据库更新后,
userDao.getUser(userId)会再次发射新的数据,这个新数据会被repo.getUser中的collect捕获并再次emit出去。这样,UI 就能收到更新后的数据。 - 网络错误被捕获处理(这里只是打印,实际中可能需要更精细处理,如发射错误状态)。
- 首先通过
ViewModel中的user流:userId是一个StateFlow,持有当前要加载的userId。userId.flatMapLatest { id -> ... }:当userId改变时,flatMapLatest会取消之前id对应的数据加载流(包括网络请求和数据库观察),并立即开始加载新id的数据。.stateIn(...):- 第一个
stateIn(在flatMapLatest内部):将repo.getUser(id)这个冷流转换成一个热 的StateFlow(或SharedFlow),在viewModelScope内共享其结果。SharingStarted.WhileSubscribed(5000)表示当没有收集者时,5 秒后停止上游流(节省资源)。 - 第二个
stateIn(最外层):将flatMapLatest产生的流(本身也是一个StateFlow)再次转换为最终的StateFlow暴露给 UI。SharingStarted.Eagerly或WhileSubscribed根据需求选择。
- 第一个
- UI 只需要收集
user这个StateFlow即可。它会自动在userId变化时切换数据源,并总是提供最新的用户数据(来自数据库,并由网络更新)。
8. 总结 (Conclusion)
Kotlin Flow 提供了一个强大、灵活且 Kotlin 协程原生的方式来处理异步数据流。它的核心优势包括:
- 声明式 & 可组合性: 使用丰富的操作符链式构建数据处理管道。
- 结构化并发: 生命周期管理安全,避免泄漏。
- 背压支持: 通过挂起机制优雅处理生产者-消费者速度不匹配。
- 冷流/热流清晰区分: 通过
Flow,SharedFlow,StateFlow满足不同场景。 - 与协程深度集成: 无缝使用
suspend函数和协程上下文。 - 轻量级: 相比 RxJava,是更 Kotlin 风格的选择,标准库依赖小。
何时使用 Flow:
- 处理来自数据库(Room Flow)、网络(Retrofit + suspend)、传感器、UI 事件等的异步数据序列。
- 需要复杂的数据转换、过滤、合并。
- 实现响应式 UI(结合
StateFlow)。 - 构建事件总线(
SharedFlow)。
何时可能不需要 Flow:
- 单次异步操作(直接用
suspend函数 +async/await)。 - 简单的回调场景(
callbackFlow虽可用,但有时直接回调更简单)。 - 需要非常底层的协程间通信原语(考虑
Channel)。
掌握 Kotlin Flow 是构建现代、响应式、高效 Kotlin 应用程序(尤其是 Android)的关键技能。从简单的冷流开始,逐步理解操作符、上下文、异常处理,再深入到 SharedFlow 和 StateFlow 的状态管理,你将能够优雅地处理各种异步数据挑战。