Kotlin Flow:构建响应式流的现代 Kotlin 之道

引言

在现代异步编程中,处理数据序列(尤其是来自异步源的数据序列,如网络响应、数据库查询、用户事件流等)是一个核心需求。Kotlin Coroutines 为我们提供了强大的底层并发原语,而 Flow 则是在 Coroutines 基础上构建的一套用于处理异步数据流的声明式、响应式 API。它设计简洁、可组合性强,并且与 Kotlin 的协程深度集成,是替代 RxJava 或 LiveData(在特定场景)的更 Kotlin 原生、更轻量级的选择。

1. Flow 的核心概念 (The Essence)

  1. 什么是 Flow?

    • Flow 代表一个异步计算的数据序列
    • 它是 "冷流"(Cold Stream) :Flow 的构建器(如 flow { ... })定义了一个数据序列的生产过程,但这个生产过程只会在收集器(collect)开始收集数据时才会启动 ,并且每次收集都会启动一个新的独立执行过程(类似于 Sequence)。这与"热流"(Hot Stream)不同,热流的生产过程独立于收集者存在(如 BroadcastChannelSharedFlow/StateFlow)。
    • 背压(Backpressure)友好:Flow 天然支持背压。收集者的处理速度决定了生产者的发射速度。如果收集者处理不过来,生产者会暂停发射,直到收集者准备好接收新数据。这是通过协程的挂起机制实现的。
    • 结构化并发(Structured Concurrency) :Flow 的生命周期紧密绑定到收集它的协程作用域(CoroutineScope)。当收集协程被取消时,Flow 的生产过程也会被自动取消,资源得到妥善释放。
  2. 关键组件:

    • 生产者(Producer) :负责产生数据。使用 flow { ... } 构建器、flowOf(...) 或 扩展函数(如 .asFlow())创建。
    • 中间操作符(Intermediate Operators) :在数据从生产者流向消费者的过程中,对数据流进行转换、过滤、组合等操作(如 map, filter, transform, zip, combine)。这些操作符通常是挂起函数返回新 Flow 的函数,它们本身不触发数据生产。
    • 消费者(Consumer / Collector) :最终接收并处理数据。主要使用 collect { ... } 终端操作符来触发整个流的执行并消费数据。其他终端操作符如 first(), toList(), count(), fold(), launchIn() 等也会触发执行。

2. 基础用法 (The Basics)

  1. 创建 Flow:

    • flow { ... } 构建器:最常用。内部使用 emit(value) 函数发送值,可以调用其他挂起函数。
    kotlin 复制代码
    fun simpleFlow(): Flow<Int> = flow {
        println("Flow started")
        for (i in 1..3) {
            delay(100) // 模拟异步工作
            emit(i)   // 发送值
        }
    }
    • flowOf(...):快速创建一个发射固定值的 Flow。
    kotlin 复制代码
    val numberFlow: Flow<Int> = flowOf(1, 2, 3, 4)
    • .asFlow() 扩展函数:将集合、范围、序列等转换为 Flow。
    kotlin 复制代码
    (1..5).asFlow()
    listOf("a", "b", "c").asFlow()
  2. 收集 Flow (触发执行):

    • 使用 collect 终端操作符。
    kotlin 复制代码
    runBlocking {
        simpleFlow().collect { value -> // 开始收集,触发生产者执行
            println("Collected $value")
        }
    }
    // 输出:
    // Flow started
    // Collected 1 (延迟约100ms)
    // Collected 2 (延迟约100ms)
    // Collected 3 (延迟约100ms)

    注意 runBlocking 仅用于示例,在主线程阻塞等待。实际应用中应在合适的协程作用域(如 viewModelScope, lifecycleScope)内收集。

  3. 常用中间操作符:

    • map: 转换每个元素。
    • filter: 过滤元素。
    • transform: 更灵活的转换,可以发射任意次(包括零次)任意值,甚至改变流类型。
    • take: 只取前 n 个元素。
    • onEach: 在每次发射前执行一个动作(不改变元素)。
    • catch: 捕获上游操作符中的异常。
    kotlin 复制代码
    runBlocking {
        (1..5).asFlow()
            .filter { it % 2 == 0 } // 只取偶数
            .map { it * it }        // 平方
            .onEach { println("Processing $it") } // 处理前打印
            .collect { println("Result: $it") }
    }
    // 输出:
    // Processing 4
    // Result: 4
    // Processing 16
    // Result: 16

3. 流上下文与异常处理 (Context & Exception Handling)

  1. 流的上下文继承 (Context Preservation):

    • 默认情况下,Flow 的生产者代码(flow { ... } 内部)会继承 调用 collect 的协程的 CoroutineContext
    • 这通常意味着生产者和消费者在同一个线程/调度器上运行。如果生产者是 CPU 密集型或阻塞 IO,可能会阻塞消费者线程。
  2. flowOn 操作符:

    • 用于改变上游操作符(在它之前的操作符)的执行上下文。
    • 它创建一个新的协程在指定的 CoroutineContext(通常是 Dispatchers)中运行上游代码。
    • 下游操作符(包括 collect)的上下文不受影响。
    kotlin 复制代码
    fun simpleFlow(): Flow<Int> = flow {
        println("Emitting on ${Thread.currentThread().name}") // 在 IO 线程
        for (i in 1..3) {
            delay(100)
            emit(i)
        }
    }.flowOn(Dispatchers.IO) // 改变上游(flow构建器)的上下文
    
    runBlocking {
        simpleFlow()
            .map { // 这个 map 在收集器的上下文运行 (DefaultDispatcher-worker-1 或 main)
                println("Mapping $it on ${Thread.currentThread().name}")
                it * it
            }
            .collect { // 也在收集器的上下文
                println("Collecting $it on ${Thread.currentThread().name}")
            }
    }
    // 输出示例:
    // Emitting on DefaultDispatcher-worker-1
    // Mapping 1 on main @coroutine#1
    // Collecting 1 on main @coroutine#1
    // ... (注意:生产在IO线程,消费转换在主线程)
  3. 异常处理:

    • catch 操作符 :用于捕获上游操作符中发生的异常。它允许你处理异常、发出错误值、或重新抛出。
    • try/catch :可以包裹 collect 或其他终端操作符来捕获消费者端的异常。
    • 声明式处理catch 是处理生产者异常的首选声明式方式。
    kotlin 复制代码
    fun faultyFlow(): Flow<String> = flow {
        emit("One")
        throw RuntimeException("Oops!")
        emit("Two") // 不会执行
    }
    
    runBlocking {
        faultyFlow()
            .catch { e -> // 捕获上游异常
                println("Caught exception: $e")
                emit("Fallback") // 发射一个回退值
            }
            .collect { value -> println(value) }
    }
    // 输出:
    // One
    // Caught exception: java.lang.RuntimeException: Oops!
    // Fallback

4. 高级操作符与组合 (Advanced Operators & Composition)

  1. zip:

    • 将两个 Flow 的对应位置的元素组合成一个新元素(通过提供的转换函数)。它等待两个 Flow 都发射了对应的元素后才组合并向下游发射。
    • 如果两个 Flow 长度不同,结果 Flow 的长度等于较短的那个 Flow 的长度。
    kotlin 复制代码
    val nums = (1..3).asFlow().onEach { delay(300) }
    val strs = flowOf("One", "Two", "Three").onEach { delay(400) }
    
    runBlocking {
        nums.zip(strs) { a, b -> "$a -> $b" }
            .collect { println(it) }
    }
    // 输出 (间隔约400ms):
    // 1 -> One
    // 2 -> Two
    // 3 -> Three
  2. combine:

    • 每当任意一个 Flow 发射新值时,就将两个 Flow 最新的元素组合成一个新元素(通过提供的转换函数)发射出去。
    • 结果 Flow 发射的频率取决于两个源 Flow 中发射更频繁的那个。
    • 非常适合需要根据多个流最新状态更新 UI 或进行计算的场景。
    kotlin 复制代码
    val nums = (1..3).asFlow().onEach { delay(300) } // 300ms, 300ms, 300ms
    val strs = flowOf("A", "B", "C").onEach { delay(400) } // 400ms, 400ms, 400ms
    
    runBlocking {
        nums.combine(strs) { a, b -> "$a$b" }
            .collect { println(it) }
    }
    // 可能的输出顺序:
    // 1A (在300ms,strs 第一个还没发,但 combine 要求每个流至少有一个值才开始?实际上combine会在两个流都有初始值后开始)
    // 2A (在600ms: nums 发2, strs 最新还是A)
    // 2B (在800ms: strs 发B, nums 最新是2)
    // 3B (在900ms: nums 发3, strs 最新是B)
    // 3C (在1200ms: strs 发C, nums 最新是3)

    (图示:combine vs zip 的发射时机差异)

    lua 复制代码
    nums:      ----1----2----3|
    strs:      --------A--------B--------C|
    
    zip:       --------(1,A)--------(2,B)--------(3,C)|
    combine:   ----1A--2A--2B--3B--3C|
    (时间线简化示意)
  3. flatMapConcat, flatMapMerge, flatMapLatest:

    • 用于处理"Flow of Flows"的场景。它们接收一个 Flow(outerFlow)和一个转换函数(该函数将 outerFlow 的每个元素转换为一个新的 Flow innerFlow),然后将这些 innerFlow "展平"(flatten) 成单个结果 Flow。区别在于如何订阅和处理内部的 innerFlow
      • flatMapConcat : 顺序 执行。等待当前的 innerFlow 完全完成 后,才开始收集下一个 innerFlow。结果流中的元素顺序严格对应 outerFlow 的元素顺序。
      • flatMapMerge : 并发 执行。同时收集所有已发射的 innerFlow(并发数量可通过 concurrency 参数限制)。结果流中的元素顺序是任意的,取决于各个 innerFlow 的完成速度。
      • flatMapLatest : 最新优先 。每当 outerFlow 发射一个新元素时,它就会取消 掉前一个仍在运行的 innerFlow 的收集,并立即开始收集新的 innerFlow。只关心最新的值。
    kotlin 复制代码
    fun requestFlow(i: Int): Flow<String> = flow {
        emit("$i: First")
        delay(500)
        emit("$i: Second")
    }
    
    runBlocking {
        // flatMapConcat
        (1..3).asFlow().onEach { delay(100) }
            .flatMapConcat { requestFlow(it) }
            .collect { println("Concat: $it") }
    
        // flatMapMerge (concurrency = 2)
        (1..3).asFlow().onEach { delay(100) }
            .flatMapMerge(2) { requestFlow(it) }
            .collect { println("Merge: $it") }
    
        // flatMapLatest
        (1..3).asFlow().onEach { delay(100) }
            .flatMapLatest { requestFlow(it) }
            .collect { println("Latest: $it") }
    }
    // flatMapConcat 输出 (顺序):
    // Concat: 1: First
    // Concat: 1: Second
    // Concat: 2: First
    // Concat: 2: Second
    // Concat: 3: First
    // Concat: 3: Second
    
    // flatMapMerge 输出 (可能顺序, 1和2并发):
    // Merge: 1: First
    // Merge: 2: First
    // Merge: 1: Second
    // Merge: 2: Second
    // Merge: 3: First
    // Merge: 3: Second
    
    // flatMapLatest 输出 (当2发射时取消1的第二个值,当3发射时取消2的第二个值):
    // Latest: 1: First
    // (100ms后2发射,取消1)
    // Latest: 2: First
    // (100ms后3发射,取消2)
    // Latest: 3: First
    // Latest: 3: Second

5. 状态容器:SharedFlow 与 StateFlow (State Holders)

Flow 默认是冷的,每个收集器获得独立的流执行。但在许多场景(如状态管理、事件广播),我们需要热的、可共享的数据流:

  1. SharedFlow:

    • 一个热流。数据生产独立于收集者的存在。即使没有收集者,生产者也可以发射数据(可配置缓存)。
    • 多个收集者共享同一个数据流 。新的收集者默认只能收到订阅之后 发射的数据(除非配置了 replay)。
    • 关键配置参数:
      • replay: 缓存多少条历史数据给新的收集者。replay = 0 表示新收集者只收新数据。
      • extraBufferCapacity: 在已有缓存 (replay) 之外,还能额外缓存多少条数据(当收集者跟不上时)。
      • onBufferOverflow: 当缓存区满时的策略(BufferOverflow.SUSPEND 挂起生产者(默认), DROP_OLDEST 丢弃最老, DROP_LATEST 丢弃最新)。
    • 创建: 使用 MutableSharedFlow 作为可写的共享流,并通过 .asSharedFlow() 将其暴露为只读的 SharedFlow 给消费者。
    kotlin 复制代码
    class EventBus {
        // 私有可写实例,配置replay=0, 无额外缓存,溢出挂起
        private val _events = MutableSharedFlow<Event>()
        // 公开只读视图
        val events: SharedFlow<Event> = _events.asSharedFlow()
    
        suspend fun postEvent(event: Event) {
            _events.emit(event) // 挂起直到事件被分发或缓存
        }
    }
    
    // 使用
    val eventBus = EventBus()
    
    // 收集者1
    viewModelScope.launch {
        eventBus.events.collect { event ->
            println("Collector1: $event")
        }
    }
    
    // 收集者2 (稍后订阅)
    viewModelScope.launch {
        delay(1000)
        eventBus.events.collect { event ->
            println("Collector2: $event")
        }
    }
    
    // 发送事件
    viewModelScope.launch {
        eventBus.postEvent(Event("A")) // 只有Collector1收到
        delay(500)
        eventBus.postEvent(Event("B")) // 只有Collector1收到
        delay(1000) // 此时Collector2已订阅
        eventBus.postEvent(Event("C")) // Collector1和Collector2都收到
    }
  2. StateFlow:

    • StateFlowSharedFlow 的一个特殊化、高度配置 的版本,专为表示可观察的状态而设计。
    • 必须有一个初始值
    • replay = 1 且固定 :新的收集者会立即 收到当前的最新状态值(即那个 replay=1 的缓存值)。
    • 值相等性 (distinctUntilChanged()) :通过 Any.equals 比较新值和当前值。只有新值与当前值不同时,才会更新状态并通知收集者。这避免了不必要的 UI 更新或计算。
    • 创建: 使用 MutableStateFlow(initialValue) 创建可写实例,并通过 .asStateFlow() 暴露为只读的 StateFlow
    kotlin 复制代码
    class CounterViewModel : ViewModel() {
        // 私有可写状态
        private val _count = MutableStateFlow(0) // 初始值0
        // 公开只读状态
        val count: StateFlow<Int> = _count.asStateFlow()
    
        fun increment() {
            _count.value++ // 更新状态 (会检查distinctUntilChanged)
        }
    }
    
    // UI (Compose 示例)
    @Composable
    fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
        val count by viewModel.count.collectAsStateWithLifecycle() // 收集状态流,感知生命周期
    
        Column {
            Text(text = "Count: $count")
            Button(onClick = { viewModel.increment() }) {
                Text("Increment")
            }
        }
    }
  3. SharedFlow vs StateFlow:

    特性 SharedFlow StateFlow
    初始值 必须有
    新订阅者获取历史 通过 replay 配置 (可0, 1, n) 总是获取最新一个值 (replay=1 固定)
    值更新条件 无条件发射 (除非缓存满策略) 仅当新值 != 当前值 (distinctUntilChanged)
    典型用途 事件总线 (单次事件) 状态容器 (可观察状态)
    是否热流
    背压处理 通过配置 (extraBufferCapacity, onBufferOverflow) 固定配置 (类似 replay=1, onBufferOverflow=SUSPEND)

    (图示:StateFlow 的初始值和 distinctUntilChanged)

    ini 复制代码
    Initial: State = 0
    Collector1 subscribes -> immediately gets 0
    
    Update: State = 1 (different) -> Collector1 notified
    Update: State = 1 (same) -> NO notification
    
    Collector2 subscribes later -> immediately gets current State (1)

6. Flow 与 Channel (Flow vs Channels)

  • Channel :是 Kotlin 协程中用于协程间通信原语 。它提供了一种在不同协程之间安全传递元素的方式(生产-消费模式)。Channel 可以有多个发送者 (send) 和多个接收者 (receive)。Channel 本身更底层。
  • Flow :建立在协程(可能用到 Channel 内部实现某些操作符如 buffer)之上的声明式、响应式数据流 API 。它专注于数据的转换和处理管道 ,通过 collect 消费。Flow 通过挂起机制天然处理背压。
  • 关系与选择:
    • Flow 更适合构建数据处理管道和响应式流。它是处理异步数据序列的首选高级 API。
    • 当你需要非挂起的发送(trySend) 、或者需要更精细地控制元素在协程间的传递(如 rendezvous 行为)时,可能需要直接使用 Channel
    • SharedFlowStateFlow 在内部使用 Channel 实现,但它们提供了更高级别的抽象。
    • 建议 :优先使用 Flow,特别是 SharedFlow/StateFlow 满足状态/事件需求。仅在 Flow API 无法直接满足底层通信原语需求时才考虑直接使用 Channel

7. 实战示例:网络请求与数据库 (Practical Example: Network + DB)

一个常见模式:从网络加载数据,缓存到数据库,UI 观察数据库中的缓存数据。

kotlin 复制代码
// 假设有 Room Dao
@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User)

    @Query("SELECT * FROM user WHERE id = :userId")
    fun getUser(userId: String): Flow<User?> // Room 支持返回 Flow!
}

// Repository
class UserRepository(private val userDao: UserDao, private val api: UserApi) {
    // 获取用户数据流。优先从DB读取,并尝试刷新网络数据。
    fun getUser(userId: String): Flow<User?> = flow {
        // 1. 先发射本地数据库中的值 (可能是null或旧数据)
        userDao.getUser(userId).collect { emit(it) }

        // 2. 尝试从网络获取最新数据
        try {
            val freshUser = api.fetchUser(userId) // 挂起网络请求
            userDao.insert(freshUser) // 更新数据库
            // Room的Flow会自动再次发射新值给上面的collect,所以这里不需要再emit
        } catch (e: Exception) {
            // 处理网络错误 (可选: 发射错误事件流? 或静默失败,依赖本地数据)
            println("Network error: ${e.message}")
        }
    }
}

// ViewModel
class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val userId = MutableStateFlow("defaultId")

    // 暴露给UI的用户状态流,基于userId变化自动切换观察对象
    val user: StateFlow<User?> = userId.flatMapLatest { id -> // 当userId变化时,取消旧请求,开始新请求
        repo.getUser(id).stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000), // 配置共享策略 (5s超时停止)
            initialValue = null
        )
    }.stateIn( // 将最终的Flow转换为StateFlow给UI
        scope = viewModelScope,
        started = SharingStarted.Eagerly, // 或 WhileSubscribed
        initialValue = null
    )

    fun loadUser(newId: String) {
        userId.value = newId
    }
}

关键点解释:

  1. userDao.getUser(userId) 返回一个 Flow<User?>。当数据库中的用户数据发生变化时(比如我们 insert 了新数据),Room 会自动通知这个 Flow 并发射最新的值。
  2. repo.getUser 方法:
    • 首先通过 userDao.getUser(userId).collect { emit(it) } 立即发射数据库中的当前值(可能为 null)。
    • 然后尝试进行网络请求。
    • 如果网络请求成功,将新数据 insert 到数据库。
    • 由于 Room Flow 的自动通知特性 ,当数据库更新后,userDao.getUser(userId) 会再次发射新的数据,这个新数据会被 repo.getUser 中的 collect 捕获并再次 emit 出去。这样,UI 就能收到更新后的数据。
    • 网络错误被捕获处理(这里只是打印,实际中可能需要更精细处理,如发射错误状态)。
  3. ViewModel 中的 user 流:
    • userId 是一个 StateFlow,持有当前要加载的 userId
    • userId.flatMapLatest { id -> ... }:当 userId 改变时,flatMapLatest 会取消之前 id 对应的数据加载流(包括网络请求和数据库观察),并立即开始加载新 id 的数据。
    • .stateIn(...)
      • 第一个 stateIn (在 flatMapLatest 内部):将 repo.getUser(id) 这个冷流转换成一个StateFlow (或 SharedFlow),在 viewModelScope 内共享其结果。SharingStarted.WhileSubscribed(5000) 表示当没有收集者时,5 秒后停止上游流(节省资源)。
      • 第二个 stateIn (最外层):将 flatMapLatest 产生的流(本身也是一个 StateFlow)再次转换为最终的 StateFlow 暴露给 UI。SharingStarted.EagerlyWhileSubscribed 根据需求选择。
    • UI 只需要收集 user 这个 StateFlow 即可。它会自动在 userId 变化时切换数据源,并总是提供最新的用户数据(来自数据库,并由网络更新)。

8. 总结 (Conclusion)

Kotlin Flow 提供了一个强大、灵活且 Kotlin 协程原生的方式来处理异步数据流。它的核心优势包括:

  1. 声明式 & 可组合性: 使用丰富的操作符链式构建数据处理管道。
  2. 结构化并发: 生命周期管理安全,避免泄漏。
  3. 背压支持: 通过挂起机制优雅处理生产者-消费者速度不匹配。
  4. 冷流/热流清晰区分: 通过 Flow, SharedFlow, StateFlow 满足不同场景。
  5. 与协程深度集成: 无缝使用 suspend 函数和协程上下文。
  6. 轻量级: 相比 RxJava,是更 Kotlin 风格的选择,标准库依赖小。

何时使用 Flow:

  • 处理来自数据库(Room Flow)、网络(Retrofit + suspend)、传感器、UI 事件等的异步数据序列。
  • 需要复杂的数据转换、过滤、合并。
  • 实现响应式 UI(结合 StateFlow)。
  • 构建事件总线(SharedFlow)。

何时可能不需要 Flow:

  • 单次异步操作(直接用 suspend 函数 + async/await)。
  • 简单的回调场景(callbackFlow 虽可用,但有时直接回调更简单)。
  • 需要非常底层的协程间通信原语(考虑 Channel)。

掌握 Kotlin Flow 是构建现代、响应式、高效 Kotlin 应用程序(尤其是 Android)的关键技能。从简单的冷流开始,逐步理解操作符、上下文、异常处理,再深入到 SharedFlowStateFlow 的状态管理,你将能够优雅地处理各种异步数据挑战。


相关推荐
站在巨人肩膀上的码农3 分钟前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗
转转技术团队11 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding13 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
呼啦啦--隔壁老王18 分钟前
屏幕旋转流程
android
gnip26 分钟前
SSE技术介绍
前端·javascript
人生何处不修行36 分钟前
实战:Android 15 (API 35) 适配 & 构建踩坑全记录
android
yinke小琪41 分钟前
JavaScript DOM节点操作(增删改)常用方法
前端·javascript
用户20187928316743 分钟前
gralde的《依赖契约法典》
android
枣把儿1 小时前
Vercel 收购 NuxtLabs!Nuxt UI Pro 即将免费!
前端·vue.js·nuxt.js
望获linux1 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件