引言
在现代异步编程中,处理数据序列(尤其是来自异步源的数据序列,如网络响应、数据库查询、用户事件流等)是一个核心需求。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:
特性 SharedFlow
StateFlow
初始值 无 必须有 新订阅者获取历史 通过 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
满足状态/事件需求。仅在Flow
API 无法直接满足底层通信原语需求时才考虑直接使用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
的状态管理,你将能够优雅地处理各种异步数据挑战。