5、Flow响应式编程:Flow是什么、冷流与热流、Flow的创建与收集、Flow的异常处理
各位同学,今天我们来聊聊Flow。说实话,刚接触Kotlin协程那会儿,我对Flow的态度是“能不用就不用”。为啥?因为当时觉得RxJava已经够用了,再学一套API有点烦。但后来在项目里重构一个下载模块时,我发现Flow的简洁和协程的天然亲和力,确实让人眼前一亮。嗯,今天我就把这几年的实战心得,掰开了讲给你听。
5.1 Flow是什么?
Flow,说白了就是Kotlin协程世界里的一种“异步数据流”。你可以把它想象成一条水管,数据像水流一样,从上游(生产者)流到下游(消费者)。
和RxJava的Observable不同,Flow是挂起函数驱动的。它不需要额外的调度器,直接跑在协程上下文里。我在项目中遇到过最典型的场景:从Room数据库查询列表,返回Flow<List<User>>,然后界面层直接collect,数据一变UI自动更新。这比手动刷新舒服多了。
- 冷流(Cold Stream)—— 没人收集就不执行
- 结构化并发 —— 跟着协程生命周期走,不会泄漏
- 背压天然支持 —— 挂起函数自动处理快慢问题
5.2 冷流与热流
这是Flow里最容易让人迷糊的地方。我刚开始也搞混过,直到有一次在聊天消息模块里踩了坑……
冷流(Cold Flow)
每次调用collect,Flow的代码块都会重新执行一遍。每个收集者都独立拥有一份完整的数据序列。举个例子:
val coldFlow = flow {
println("Flow started")
for (i in 1..3) {
emit(i)
delay(100)
}
}
// 第一个收集者
coroutineScope.launch {
coldFlow.collect { println("Collector A: $it") }
}
// 第二个收集者
coroutineScope.launch {
coldFlow.collect { println("Collector B: $it") }
}
// 输出:
// Flow started
// Collector A: 1
// Collector A: 2
// Collector A: 3
// Flow started
// Collector B: 1
// Collector B: 2
// Collector B: 3
看到了吗?两个收集者各自触发了一次“Flow started”。这就是冷流的典型行为——每个订阅者都是“从头开始”的。
热流(Hot Flow)
热流就不一样了。数据是独立于收集者存在的,不管有没有人收集,它都在那里生产。后来的收集者只能收到“加入之后”的数据。
Kotlin提供了两种热流:StateFlow 和 SharedFlow。
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须提供 | 可选 |
| 粘性 | 有(新订阅者会收到最新值) | 无(默认不粘性) |
| 去重 | 自动(值相等时不发送) | 不自动 |
| 典型场景 | UI状态持有 | 事件总线、一次性通知 |
5.3 Flow的创建与收集
创建Flow的方式有很多,我挑几个最常用的说说。
创建方式
- flow { } —— 最基础,手动emit数据
- flowOf(1, 2, 3) —— 固定数据集合
- asFlow() —— 集合、数组、Sequence直接转Flow
- callbackFlow { } —— 把回调式API转成Flow(比如传感器监听)
// 实际项目里我最常用的是 flow { } 配合 Room
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// 在ViewModel里
class UserViewModel(private val dao: UserDao) : ViewModel() {
val users: StateFlow<List<User>> = dao.getAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
收集方式
收集Flow最直接的方法就是collect。但要注意,它是挂起函数,必须在协程里调用。
// 在Activity/Fragment里
lifecycleScope.launch {
viewModel.users.collect { userList ->
adapter.submitList(userList)
}
}
嗯,这里有个坑。我曾经直接在lifecycleScope.launch里collect,结果页面进入后台时协程还在跑,导致界面更新崩溃。后来改用repeatOnLifecycle才解决:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.users.collect { ... }
}
}
5.4 Flow的异常处理
Flow里的异常处理,和普通协程不太一样。你不能直接try-catch整个collect块,因为Flow可能是无限流。我刚开始写的时候也犯过这个错。
使用catch操作符
这是最推荐的方式。catch可以捕获上游的异常,并且不会中断整个Flow。
flow {
emit(1)
throw RuntimeException("Oops")
emit(2) // 不会执行
}.catch { e ->
println("Caught: $e")
emit(-1) // 可以发送一个兜底值
}.collect {
println(it)
}
// 输出:1, Caught: java.lang.RuntimeException: Oops, -1
使用retry操作符
网络请求失败时重试,这个我太有发言权了。曾经有个接口偶尔超时,我直接用retry(3),结果用户反馈卡死……后来发现retry默认是立即重试,没有退避策略。
flow {
// 网络请求
emit(api.fetchData())
}.retry(retries = 3) { cause ->
if (cause is IOException) {
delay(1000) // 简单退避
true // 重试
} else {
false // 其他异常不重试
}
}.catch { e ->
emit(emptyList()) // 最终兜底
}.collect { ... }
onCompletion 收尾
不管Flow正常结束还是异常结束,onCompletion都会执行。我习惯用它来关闭加载状态。
flow { ... }
.onCompletion { println("Flow finished") }
.catch { ... }
.collect()
知识体系总览
下面这张图,是我自己总结的Flow知识脉络。你可以把它当作学习地图。
好了,关于Flow的基础知识就聊到这儿。冷流热流的选择、创建收集的姿势、异常处理的套路,这些都是在项目里实打实会遇到的。下次你写网络层或者数据库层的时候,不妨试试用Flow替代回调,你会发现代码清爽很多。