Flow基本用法:flow builder,emit,collect

Flow 说白了就是 Kotlin 协程里的一种冷数据流。冷是什么意思?就是它不会事先产生数据,只有当你主动去收集(collect)的时候,它才开始干活。我个人习惯把它想象成一个“按需生产”的管道,没人拧开水龙头,它就不流水。

💡 核心三板斧:flow { ... } 创建管道,emit(value) 往管道里丢数据,collect { ... } 在终端打开水龙头接数据。就这么简单。

1. flow builder — 创建管道的起点

用 flow 构建器启动一个协程作用域,在里面写发射逻辑。我刚开始接触时总以为它和 suspend 函数一样,直接在函数里 return 数据就行。后来发现完全不是一回事——Flow 是流式返回,不是一次性返回。

val myFlow: Flow<Int> = flow {
    // 这里运行在协程上下文中
    for (i in 1..3) {
        delay(100)          // 模拟耗时
        emit(i)             // 每次发射一个数
    }
}

要注意,flow builder 内部的代码默认是顺序执行的,delay 不会阻塞线程,只挂起协程。这点和 suspend 函数一样。

⚠️ 我曾经踩过一个坑:在 flow builder 里直接调用阻塞式 IO(比如 Thread.sleep),会导致整个协程被卡住。记得换成 delay 或 withContext(Dispatchers.IO) 包装一下。

2. emit — 往管道里扔东西

emit 是 FlowCollector 的扩展函数,本质就是把值交给下游。你可以在 flow builder 里任意调用,甚至多次循环发射。每个 emit 都会挂起当前协程,直到下游 collector 处理完这个值。

关键点:emit 是挂起函数,不是同步调用。这意味着 Flow 天然支持反压——如果下游处理速度慢,上游 emit 会被自动挂起,不会积压数据。

flow {
    emit(1)
    delay(500)
    emit(2)
    emit(3)  // 连续发射,下游按顺序接到
}

我记得有一次做实时定位,每隔 1 秒 emit 一次经纬度,下游在 collect 里做 UI 更新。因为 emit 是挂起的,就算上游想快点发射,下游没处理完它也快不了。这就避免了数据堆积。

3. collect — 终端收集者

collect 是终端操作符,它会挂起当前协程,直到 Flow 结束(或者协程取消)。每次下游 collect 拿到一个值,可以执行任意逻辑。注意 collect 也是一个挂起函数,只能在协程里调用。

lifecycleScope.launch {
    myFlow.collect { value ->
        Log.d("Flow", "收到: $value")
        // 在这里更新 UI 或写数据库
    }
}

你想想看,collect 本身就是一个冷启动的开关——只要你不调用 collect,Flow 里的一行代码都不会执行。所以它很适合做懒加载数据流。

🔥 实战经验:我在写网络请求库时,用 Flow 包装 OkHttp 的 ResponseBody,每次读取到一部分字节就 emit 出去,collect 里负责写入文件或显示进度条。流程很清晰,而且天然支持取消(协程取消时 collect 自动终止)。

4. 完整示例:从数据源到 UI

下面是个模拟股票价格刷新的例子,每隔 500ms 生成一个随机价格。看看 flow builder、emit、collect 怎么配合。

fun stockPriceFlow(): Flow<Double> = flow {
    while (true) {
        val price = 100.0 + Random.nextDouble() * 10
        emit(price)
        delay(500)
    }
}

// 在 ViewModel 或 Activity 里收集
viewModelScope.launch {
    stockPriceFlow().collect { price ->
        // 更新 UI(注意要切换到主线程)
        withContext(Dispatchers.Main) {
            textView.text = "当前价格: $%.2f".format(price)
        }
    }
}

这个例子很简单,但藏着几个要点:

  • while(true) 循环不会死循环——因为 emit 会挂起,collect 取消后整个协程就释放了。
  • 千万不要在 collect 里做耗时操作!collect 是挂起函数但不自动切换调度器。如果里面要写数据库,记得用 withContext(Dispatchers.IO)。我吃过苦头,在 collect 里直接做文件读写把主线程拖垮了。

💡 小技巧:如果要取消收集,直接取消所在的协程作用域(比如 viewModelScope.cancel())就行。Flow 会自动感知取消并停止发射,不需要额外写 try-catch。

5. 知识结构速查表

组件作用挂起函数?调用位置
flow { ... }创建冷数据流是(内部代码块运行在协程)普通函数或协程内
emit(value)向流中发射一个值是(会挂起直到下游消费)只能在 flow builder 内部
collect { ... }终端消费流中的值是(会挂起直到流结束)只能在协程内调用

6. 不要踩的坑

  • 不要在 collect 外使用 emit:emit 只能在 flow builder 或 FlowCollector 扩展里调用,普通函数里用会报错。
  • collect 也要记得处理异常:如果 flow 内部抛异常,collect 会直接收到,建议用 catch 操作符包裹。
  • 连续多次 collect:同一个 Flow 对象可以被多次 collect 吗?当然可以,但每次都是独立的新流,从头开始执行 flow builder。不会共享状态。

嗯,说了这么多,其实 Flow 的基本用法就围绕这三个关键词转。我个人把 flow builder 比作工厂,emit 是传送带上的产品,collect 是工厂门口的收货人。一旦收货人走了(协程取消),传送带也停了。

📌 最简洁的总结:用 flow { } 定义生产逻辑,用 emit 逐个吐出数据,用 collect 在终端一个个消费。三句话搞定 Flow 入门。

Flow 基本生命周期 flow { ... } 创建冷数据流 emit(value) 发射数据 collect { } 消费数据 协程取消 → collect 终止 → flow 自动停止 冷流:按需生产 挂起式发射(反压) 取消安全

这张图把 Flow 的三大步骤串起来了:由 flow builder 启动,经过 emit 发射,最终由 collect 消费。右边还展示了取消机制——协程取消时 collect 停止,上游 emit 自动释放资源。底下的三个特性(冷流、反压、取消安全)是 Kotlin Flow 区别于其他响应式库的核心优势。