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提供了两种热流:StateFlowSharedFlow

特性 StateFlow SharedFlow
初始值 必须提供 可选
粘性 有(新订阅者会收到最新值) 无(默认不粘性)
去重 自动(值相等时不发送) 不自动
典型场景 UI状态持有 事件总线、一次性通知
我的建议:UI层状态管理无脑用StateFlow。事件传递(比如Snackbar显示)用SharedFlow,记得配置replay=0,避免重播。

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 { ... }
    }
}
注意:不要在Fragment的onViewCreated里直接launch一个协程去collect Flow。如果页面重建,旧的协程可能泄漏。用repeatOnLifecycle或者flowWithLifecycle更安全。

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()
避坑指南:我曾经在catch之后又加了一个catch,结果第二个catch永远收不到异常——因为第一个catch已经消费掉了。记住,catch只捕获上游的异常,下游的异常它管不了。如果collect块里会抛异常,用try-catch包一下collect。

知识体系总览

下面这张图,是我自己总结的Flow知识脉络。你可以把它当作学习地图。

Flow 响应式编程 冷流 vs 热流 冷流:flow {} 热流:StateFlow 热流:SharedFlow 创建与收集 flowOf / asFlow callbackFlow collect / stateIn 异常处理 catch 操作符 retry 重试 onCompletion 核心三要素:冷热流 · 创建收集 · 异常处理 每个分支都可以深入展开,但记住这三大块就抓住了Flow的骨架

好了,关于Flow的基础知识就聊到这儿。冷流热流的选择、创建收集的姿势、异常处理的套路,这些都是在项目里实打实会遇到的。下次你写网络层或者数据库层的时候,不妨试试用Flow替代回调,你会发现代码清爽很多。

公众号:蓝海资料掘金营,微信deep3321