【Kotlin 协程修仙录 · 元婴境 · 初阶】 | 冷流初啼:Flow 的基础与响应式编程的入门

前言

金丹已凝,你对协程的并发、调度、异常处理已驾轻就熟。你能用 async 并发请求多个接口,能用 supervisorScope 隔离局部失败,能用 withContext 自如切换线程。你写的代码,既安全又优雅。

但现在,你遇到了一个新的挑战:

产品经理要求你实现一个"实时搜索"功能------用户在输入框中每输入一个字符,就发起一次网络请求,展示搜索建议。你还得处理各种边界:去抖动(避免过快请求)、取消上一个未完成的请求、错误重试、结果去重......

你尝试用 launch + Channel 实现,发现代码迅速膨胀。你想起 RxJava 里有 debounceswitchMapdistinctUntilChanged 等操作符,但引入整个 RxJava 又显得太重。

Kotlin 协程给出的答案,是一个叫做 Flow 的响应式流框架。

FlowKotlin 协程官方提供的冷流 实现。它允许你以声明式的方式处理连续发射的多个值,并天然集成了协程的挂起、取消、异常处理机制。

如果说 suspend 函数是"单次异步调用"的终极方案,那么 Flow 就是"连续数据流"的终极方案。它填补了协程生态中响应式编程的最后一块拼图。

本讲是元婴境的初阶修炼。你将:

  • 理解 Flow 是什么,以及它与 LiveDataRxJavaSequence 的区别。
  • 掌握冷流的本质:为什么 Flow 是"懒加载"的?
  • 学会用 flow {} 构建器创建自己的数据流。
  • 了解 collect 终端操作符和流的基本生命周期。

准备好元婴初啼,感受数据流动了吗?我们开始。

千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意


什么是 Flow

在 Kotlin 协程的官方定义中:

Flow<T> 是一个冷流 接口,代表一个可以异步、按顺序发射多个值 的数据流。它使用挂起函数来生产与消费数据 ,天然支持背压结构化并发

Flow 并不是 Kotlin 协程的独创概念。如果你熟悉 RxJava,可以把 Flow 理解为协程版的 Observable,但更轻量、与协程生命周期深度绑定、API 更简洁。

flowchart LR subgraph Producer[生产者] P1[发射值1] P2[发射值2] P3[发射值3] end subgraph Flow[Flow 管道] direction LR Op1[操作符1] Op2[操作符2] end subgraph Consumer[消费者] C[collect 收集] end Producer --> Flow --> Consumer style Producer fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style Flow fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Consumer fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style P1 fill:#c8e6c9 style P2 fill:#c8e6c9 style P3 fill:#c8e6c9 style C fill:#90caf9

Flow 的核心特征

  1. 冷流Flow 仅在被收集(collect)时才开始执行。每次收集都会触发一次全新的数据生产。
  2. 挂起:生产者和消费者都可以是挂起函数,天然支持异步操作而不阻塞线程。
  3. 可取消Flow 的收集过程响应协程的取消信号,无需额外处理。
  4. 结构化并发Flow 的生命周期受收集它的协程作用域约束。

为什么需要 FlowFlow vs 其他方案

在 Flow 出现之前,Android 开发者处理连续数据流的方案主要有三种。它们各有痛点。

方案 优点 痛点 与协程集成
回调接口 简单直接 回调地狱,生命周期需手动管理 需手动适配
LiveData 生命周期感知,自动清理 操作符少,不支持挂起,强制主线程处理数据 liveData 构建器
RxJava 操作符极丰富,功能强大 学习曲线陡,依赖重,响应式类型繁多 rxjava-coroutines 适配
Flow 协程原生,操作符丰富,轻量 相对较新,生态仍在完善 无缝集成

Flow 的核心价值 :它让你用一套统一的协程心智模型 处理所有异步场景------单次异步用 suspend fun,连续数据流用 Flow。不再需要在不同框架之间来回切换。


冷流的本质:为什么 Flow 是"懒"的?

Flow 最区别于传统 LiveDataChannel 的特性是:它是冷的

冷流 意味着:Flow 的代码块在被收集(collect)之前不会执行。每一次收集都会触发一次全新的数据生产,互不干扰。

用日常生活的类比:

  • 热流广播电台。无论有没有人收听,电台都在持续发射信号。后来打开收音机的人,只能听到此刻之后的内容。
  • 冷流点播视频。只有当用户点击"播放"时,视频才开始加载和播放。每个用户点击播放,都会触发一次独立的视频流。
sequenceDiagram participant Producer as 生产者 (flow { }) participant Flow as Flow 对象 participant Collector1 as 收集者1 participant Collector2 as 收集者2 Note over Flow: Flow 被创建,但未执行 Collector1->>Flow: collect { } Flow->>Producer: 触发生产(第1次) Producer-->>Collector1: 发射 1, 2, 3 Collector2->>Flow: collect { } Flow->>Producer: 触发生产(第2次) Producer-->>Collector2: 发射 1, 2, 3

代码验证

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simpleFlow(): Flow<Int> = flow {
    println("Flow 开始生产")
    emit(1)
    emit(2)
    emit(3)
}

fun main() = runBlocking {
    val flow = simpleFlow()
    println("Flow 已创建,但尚未生产")
    
    println("第一次收集:")
    flow.collect { value -> println("收集到:$value") }
    
    println("第二次收集:")
    flow.collect { value -> println("收集到:$value") }
}

输出:

css 复制代码
Flow 已创建,但尚未生产
第一次收集:
Flow 开始生产
收集到:1
收集到:2
收集到:3
第二次收集:
Flow 开始生产
收集到:1
收集到:2
收集到:3

每次 collect 都会触发 flow {} 块的重新执行。这就是冷流的核心特征。

为什么冷流很重要?

  • 资源节省:不收集就不执行,不会白白消耗 CPU/网络。
  • 可重放:每次收集都是独立的数据流,适合处理"每个订阅者都需要最新数据"的场景(如网络请求)。
  • 线程安全:不同收集者的数据生产彼此隔离,无需担心并发问题。

flow {} 构建器:创造你的第一个 Flow

flow {} 是创建 Flow 最基础的构建器。它的签名如下:

kotlin 复制代码
public fun <T> flow(
    block: suspend FlowCollector<T>.() -> Unit
): Flow<T>

block 内部,你可以使用 emit() 函数发射值。emit 是一个挂起函数,这意味着 Flow 的生产过程天然支持异步操作。

kotlin 复制代码
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

/**
 * 创建一个每秒发射一个数字的 Flow
 */
fun countdownFlow(from: Int): Flow<Int> = flow {
    for (i in from downTo 1) {
        delay(1000) // 模拟耗时操作
        emit(i)     // 发射当前值
    }
}

ViewModel 中使用 Flow

kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class CountdownViewModel : ViewModel() {

    // 暴露 Flow,而非 LiveData
    val countdownFlow: Flow<Int> = countdownFlow(10)

    fun startCountdown() {
        viewModelScope.launch {
            countdownFlow.collect { value ->
                // 这里的代码会在每个值发射时执行
                println("倒计时:$value")
                // 更新 UI 状态(后续讲 StateFlow 时会优化)
            }
        }
    }
}

Flow 的生命周期与取消

Flow 的收集过程天然响应协程的取消。当收集 Flow 的协程被取消时,Flow 的生产会停止。

kotlin 复制代码
fun main() = runBlocking {
    val job = launch {
        countdownFlow(10).collect { value ->
            println(value)
        }
    }
    
    delay(3500) // 等待 3.5 秒
    job.cancelAndJoin() // 取消收集协程
    println("收集已取消")
}
// 输出只会到 7 左右,因为协程被取消了
sequenceDiagram participant Job as 收集协程 participant Flow as Flow participant Producer as 生产者 (flow { }) Job->>Flow: collect Flow->>Producer: 启动生产 loop 每发射一个值 Producer->>Producer: delay + emit Producer-->>Job: 值 end Job->>Job: 外部调用 cancel() Job->>Producer: 传播取消信号 Producer-->>Producer: 下一个挂起点抛出 CancellationException Producer-->>Flow: 停止生产 Flow-->>Job: 收集结束

collect 终端操作符:点燃冷流的火种

collectFlow 最基础的终端操作符 。它的作用是从 Flow 中收集每一个发射的值,并对每个值执行指定的操作。

终端操作符 :触发 Flow 开始执行的操作符。没有终端操作符,Flow 永远不会运行。

collect 是一个挂起函数,它会挂起当前协程 直到 Flow 完成(发射完所有值或抛出异常)。

kotlin 复制代码
public suspend fun Flow<*>.collect(collector: FlowCollector<*>)

除了 collectFlow 还提供了一些便捷的终端操作符:

操作符 作用 适用场景
collect 收集每一个值 需要处理每一个发射值
collectLatest 只处理最新的值,如果新值到来时上一个值还没处理完,则取消上一个处理 实时搜索、UI 刷新
toList 将 Flow 的所有值收集到一个 List 中 需要一次性获取所有结果
toSet 收集到一个 Set 中 需要去重
first 只收集第一个值,然后取消 Flow 只关心首次结果
reduce 对值进行累加计算 求和、拼接等
fold 带初始值的累加 复杂聚合
kotlin 复制代码
// 使用 toList 一次性收集
suspend fun getAllNumbers(): List<Int> = countdownFlow(5).toList()
// 返回 [5, 4, 3, 2, 1]

// 使用 first 只取第一个
suspend fun getFirstNumber(): Int = countdownFlow(5).first()
// 返回 5

终端操作符分类图:


实战:用 Flow 实现实时搜索

让我们用一个完整的 Android 实战案例来巩固 Flow 的基础用法。场景是:用户在 TextField 中输入关键词,应用实时展示搜索建议。

关键需求

  1. 每次输入都触发搜索请求。
  2. 避免过快触发(去抖动)。
  3. 取消上一个未完成的请求。
  4. 忽略重复的关键词。

这些在 Flow 的世界里只需要几个操作符即可实现(本讲先展示基础架构,操作符细节将在元婴境·中阶深入)。

kotlin 复制代码
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

class SearchViewModel(
    private val searchRepo: SearchRepository
) : ViewModel() {

    // 输入框的文本变化,使用 MutableStateFlow 作为输入源
    private val queryFlow = MutableStateFlow("")
    
    // 搜索结果状态
    var searchResult by mutableStateOf<List<String>>(emptyList())
        private set
    var isLoading by mutableStateOf(false)
        private set

    init {
        viewModelScope.launch {
            queryFlow
                .debounce(300) // 去抖动:300ms 内无新输入才向下传递
                .filter { it.length >= 2 } // 至少两个字符才搜索
                .distinctUntilChanged() // 忽略连续相同的关键词
                .flatMapLatest { query -> // 切换为新请求,取消旧的
                    flow {
                        isLoading = true
                        emit(searchRepo.search(query))
                        isLoading = false
                    }
                }
                .catch { e -> 
                    // 处理异常
                    searchResult = emptyList()
                    isLoading = false
                }
                .collect { result ->
                    searchResult = result
                }
        }
    }

    fun onQueryChanged(newQuery: String) {
        queryFlow.value = newQuery
    }
}

配合 Compose UI:

kotlin 复制代码
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = "",
            onValueChange = { viewModel.onQueryChanged(it) },
            placeholder = { Text("输入关键词搜索") },
            modifier = Modifier.fillMaxWidth()
        )
        
        if (viewModel.isLoading) {
            LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
        }
        
        LazyColumn {
            items(viewModel.searchResult) { item ->
                Text(item, modifier = Modifier.padding(8.dp))
            }
        }
    }
}

这个示例展示了 Flow 的强大:用声明式的链式调用,清晰地表达了复杂的异步逻辑。这正是 Flow 相比手动管理协程的压倒性优势。

flowchart LR subgraph Input[输入源] TF[TextField 输入] end subgraph FlowPipeline[Flow 处理管道] Q[queryFlow] D[debounce 300ms] F[filter length>=2] DIST[distinctUntilChanged] FLAT[flatMapLatest] end subgraph Output[结果] UI[更新 UI] end TF --> Q --> D --> F --> DIST --> FLAT --> UI style Input fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style FlowPipeline fill:#fff3e0,stroke:#f57c00,stroke-width:2px style Output fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

常见错误与避坑指南

错误 1:在 flow {} 块内调用挂起函数前未处理取消

kotlin 复制代码
// ❌ 错误:如果在 emit 前执行了不支持取消的阻塞操作,协程无法及时响应取消
fun badFlow(): Flow<Int> = flow {
    Thread.sleep(5000) // 阻塞,不响应取消!
    emit(1)
}

正确做法 :使用挂起函数 delay 或支持取消的 API。

kotlin 复制代码
fun goodFlow(): Flow<Int> = flow {
    delay(5000) // 挂起,响应取消
    emit(1)
}

错误 2:在 ViewModel 中直接 collect 而不使用 launch

kotlin 复制代码
// ❌ 错误:collect 是挂起函数,不能直接在 init 中调用
class MyViewModel : ViewModel() {
    init {
        someFlow.collect { } // 编译错误!
    }
}

正确做法 :在 viewModelScope.launch 中收集。

kotlin 复制代码
class MyViewModel : ViewModel() {
    init {
        viewModelScope.launch {
            someFlow.collect { }
        }
    }
}

错误 3:混淆冷流和热流,期望多次收集共享同一数据源

kotlin 复制代码
val flow = flow { emit(api.getData()) }
// 两次收集会触发两次网络请求!
flow.collect { }
flow.collect { }

如果你需要共享同一数据源,应该使用 StateFlowSharedFlow(热流),这将在化神境讲解。


最佳实践

  1. 暴露 Flow,不暴露可变状态 :在 ViewModel 中,对外暴露 Flow<T> 而非 MutableStateFlow,保持封装性。

  2. 使用 catch 操作符统一处理异常 :不要在每次 collect 处写 try-catch,而是用 flow.catch { } 在管道中处理。

  3. 在 ViewModel 中使用 stateIn 将冷流转换为热流:避免重复的网络请求,确保 UI 重组时不会触发多次收集。

  4. 理解冷流的"每次收集独立执行" :如果你需要缓存结果,使用 stateInshareIn

  5. Flow 的构建器内不要做太重的工作flow {} 块应该专注于发射值,复杂逻辑封装在单独的挂起函数中。


总结与下回预告

恭喜,你已成功凝结元婴,掌握了 Flow 的基础用法!

本讲核心收获

  • Flow 是协程原生的冷流实现,用于处理连续发射的异步数据。
  • 冷流是"懒"的:只在被收集时才执行,每次收集独立运行。
  • flow {} 构建器 + emit() 发射值,collect 终端操作符触发执行。
  • Flow 天然集成协程的取消、异常处理和结构化并发。
  • Flow 相比回调、LiveData、RxJava 有显著优势,是协程生态响应式编程的首选。

在下一讲 【元婴境·中阶】 中,我们将深入学习 Flow 的中间操作符:mapfiltertransformtake 等。届时你会明白:

  • 如何像搭积木一样用操作符构建数据处理管道?
  • flatMapConcatflatMapMergeflatMapLatest 的区别与应用场景?
  • 如何处理 Flow 中的异常和完成事件?

【当前境界修为面板】

当前境界 修炼技能 修炼进度 修炼心得
元婴境 · 初阶 1、Flow 冷流心法 2、flow {} 创造术 3、collect 收集诀 当前进度25% 修为250/1000 下一突破[元婴境 · 中阶] (需领悟:Flow 操作符大全) 冷流如点播视频------不点击播放就不加载,每次播放都是从头开始。

【本讲思考题】

1、表象题:以下代码会打印什么?

kotlin 复制代码
runBlocking {
    val flow = flow {
        println("Start")
        emit(1)
        emit(2)
    }
    println("Before collect")
    flow.collect { println(it) }
    flow.collect { println(it) }
}

2、场景题 :你需要实现一个"每隔 1 秒轮询一次服务器获取最新状态,直到状态变为 COMPLETED"的功能。如何用 flow {}collect 实现?写出核心代码。

3、原理题flow {} 构建器内部是如何保证线程安全的?为什么多个收集者同时收集同一个 Flow 不会互相干扰?请结合 SafeFlow 类简述。


道友,元婴初成,你已能感知数据流动的韵律。下一讲,我们将掌握 Flow 的操作符,让数据在管道中自由变幻。元婴境·中阶见。

欢迎一键四连关注 + 点赞 + 收藏 + 评论

相关推荐
用户459892045651615 小时前
一套 KMP 多仓库版本治理工作流:用 Version Catalog + CI 把发版流水线自动化
前端·kotlin
2601_9665635217 小时前
愤怒的小鸟单机版 去广告 安卓+PC端离线纯净版 益智耐玩小游戏 不需联网 老旧手机都可以玩 怀旧手机小游戏
android·智能手机
hunterandroid17 小时前
[Android 从零到一] Retrofit 请求取消与生命周期绑定:从 Call.cancel 到协程可取消设计
android
爱跑马的程序员17 小时前
安卓专有的通信子系统-Binder IPC
android·binder·ipc·安卓间通信机制
ue星空18 小时前
【安卓逆向】为什么用Frida?
android
小驰行动派18 小时前
Camx架构全景图:从V4L2到Pipeline的完整拆解
android·camera·android camera
独隅19 小时前
KMP 全栈进化:Koog 框架打造纯 Kotlin AI Agent 实战效果
开发语言·人工智能·kotlin
hunterandroid19 小时前
[Android 从零到一] Custom View 触摸反馈与手势冲突解决
android
coderSong256820 小时前
Android | 四大组件之 BroadcastReceiver(广播接收器)
android
爱笑鱼1 天前
Android 系统启动机制(二):init.rc 不是普通脚本,service、action 和 property 怎样驱动启动?
android