1. Flow 基础与 collect 生命周期
1.1 Flow 的创建方式
Kotlin Flow 提供了三种核心创建方式,每种方式对应不同的数据生产场景:
| 创建方式 | 语法 | 适用场景 |
|---|---|---|
| flow { } | flow { emit(data) } | 通用异步数据流,可调用挂起函数 |
| flowOf() | flowOf(a, b, c) | 固定已知值的发射 |
| asFlow() | list.asFlow() | 集合/Sequence 转 Flow |
// flow { } —— 最常用的冷流创建方式
val userFlow: Flow<User> = flow {
val users = userRepository.fetchUsers() // 挂起函数
for (user in users) {
emit(user) // 发射数据到下游
}
}
// flowOf() —— 静态数据
val numberFlow = flowOf(1, 2, 3, 4, 5)
// asFlow() —— 集合转换
val nameFlow = listOf("Alice", "Bob", "Charlie").asFlow()
1.2 collect 挂起原理
collect 是冷流的终端操作,其本质是一个挂起函数。调用 collect 时,协程会被挂起,直到 Flow 生产者完成所有数据发射或协程被取消。每次 collect 都会独立触发生产者的执行,这就是冷流“每次订阅都重新开始”的核心机制。
// collect 是挂起函数,调用它的协程会被阻塞(挂起)直到 Flow 结束
lifecycleScope.launch {
flow {
emit("A")
delay(100) // 挂起 100ms
emit("B")
}.collect { value ->
// 此 lambda 在每次 emit 时被调用
Log.d("Flow", "收到: $value")
}
// 代码执行到这里时,Flow 已经结束
Log.d("Flow", "collect 完成,协程恢复")
}
⚠️ 常见误解:不要认为
collect 是“监听”,它实际上是“等待并消费”。冷流在 collect 调用时才会真正激活生产者代码,每次 collect 都对应一次全新的数据生产周期。
1.3 冷流与热流的本质区别
冷流和热流是 Flow 体系中最重要的概念区分,直接影响 collect 的耗电行为:
| 特性 | 冷流 (Cold Flow) | 热流 (Hot Flow) |
|---|---|---|
| 数据生产时机 | 每次 collect 时重新执行生产者 | 独立于 collect 持续生产 |
| 订阅者数量 | 一对一(每个 collect 独立) | 一对多(共享数据流) |
| 是否缓存数据 | 无缓存 | 可通过 replay 缓存最近值 |
| 典型类型 | flow { }、flowOf() | StateFlow、SharedFlow |
| collect 副作用 | 触发重复计算(耗电风险) | 不触发生产,仅接收 |
💡 性能启示:冷流每次 collect 都会重新执行生产者,如果生产者包含网络请求、数据库查询或传感器读数,就会产生不必要的电量消耗。因此,在 UI 层使用 StateFlow 替换冷流,是减少耗电的第一道防线。
1.4 冷流与热流结构对比图
下面这张 SVG 图清晰地展示了冷流和热流在订阅关系上的核心差异:
1.5 调试 collect 回调的基本手段
在分析和优化耗电问题前,需要先能观察到 collect 的调用次数和执行上下文:
// 调试手段1:使用 onStart / onCompletion 追踪生命周期
flow {
emit(1)
delay(50)
emit(2)
}.onStart {
Log.d("FlowDebug", "collect 开始,当前线程: ${Thread.currentThread().name}")
}.onCompletion {
Log.d("FlowDebug", "collect 结束")
}.collect {
Log.d("FlowDebug", "收到值: $it")
}
// 调试手段2:通过协程上下文判断 collect 所在作用域
lifecycleScope.launch(Dispatchers.IO) {
// collect 在 IO 线程执行,但依然会挂起当前协程
someFlow.collect { /* ... */ }
}
1.6 常见错误:重复 collect 导致多倍耗电
典型反模式:在 LaunchedEffect 内多次调用 collect,或错误地使用 repeatOnLifecycle 包裹了不该重复激活的冷流。
// ❌ 错误:每次 recomposition 都创建新的 collect
@Composable
fun BadScreen() {
val data = remember { MutableStateFlow("") }
LaunchedEffect(Unit) {
// 注意:如果这里使用冷流,每次 collect 都会触发网络请求
repository.fetchDataFlow().collect { data.value = it }
}
}
// ✅ 改进:使用 StateFlow 共享数据流,避免重复生产
@Composable
fun GoodScreen(viewModel: MyViewModel) {
val data by viewModel.uiState.collectAsState()
// collectAsState 内部使用 StateFlow,不会重复触发生产者
}
1.7 性能要点小结
- 冷流:适合单次操作或不确定是否会被消费的场景;每次 collect 都有完整的数据生产开销。
- 热流:适合 UI 层持续观察状态;collect 只消费已有数据,不触发额外计算。
- 选择策略:数据层返回冷流(保留惰性),UI 层通过
stateIn/shareIn转为热流,是减少不必要的 collect 耗电的标准做法。