Android Flow 笔记

17 阅读9分钟

🎯 Flow 的设计哲学

Flow 的设计初衷是解决 "异步数据流的统一处理" 问题。在传统的编程模型中,我们处理单个异步结果用 suspend 函数,处理多个同步值用 Sequence/Collection,但处理多个异步到达的值(如数据库观察者、传感器数据、UI 事件等)一直没有统一的工具。RxJava 试图解决这个问题,但学习曲线陡峭,且与 Kotlin 协程的集成不够自然。

Flow 的设计目标:

  • 与协程深度集成:Flow 的所有操作都是挂起函数,可以无缝使用协程的取消、结构化并发等特性。
  • 冷流:Flow 的生产者代码只在有消费者收集时才执行,避免了不必要的资源消耗。
  • 背压透明:Flow 默认是同步的,消费者处理速度直接影响生产者,但提供了缓冲区、合并等操作符来应对速度不匹配。
  • 操作符丰富:提供了与 RxJava 类似的操作符(mapfilterflatMapConcat 等),但更简洁、更协程化。
  • 可取消:Flow 遵循协程的取消机制,当收集的协程被取消时,Flow 的生产者和中间操作都会自动停止。

🔄 Flow 的核心类型与分类

Flow 主要分为两大类:冷流热流

1. 冷流 (Cold Stream)

  • 定义:每次收集都会重新执行生产者的代码,数据生产与收集一一对应。
  • 代表Flow 接口本身(通过 flow { ... } 构建)。
  • 特点
    • 无状态,每次收集独立。
    • 不会主动发射数据,直到被 collect
    • 适合封装一次性的数据源(如网络请求、数据库查询)。

2. 热流 (Hot Stream)

  • 定义:无论是否有收集者,数据都会持续产生,多个收集者共享同一份数据流。
  • 代表StateFlowSharedFlow
  • 特点
    • 有状态,可以缓存最新值或事件。
    • 多个收集者可以同时订阅,不会触发新的生产者执行。
    • 适合 UI 状态持有(StateFlow)或事件广播(SharedFlow)。

🧱 Flow 的底层原理:协程与 Channel

要深入理解 Flow,必须了解其底层依赖的两大组件:协程挂起Channel

协程挂起

Flow 的所有终端操作符(如 collect)都是挂起函数。当调用 collect 时,当前协程会被挂起,直到流中的所有数据被处理完或流被取消。中间操作符(如 map)也是挂起函数,但它们是内联或生成新的 Flow,不会直接挂起。

Channel

flow { ... } 构建器内部使用了 ProducerScope,它是一个带有 send 方法的协程作用域。当你调用 emit 时,本质上是通过 ProducerScope 内部的 Channel 将数据发送给下游。这个 Channel 是无缓冲的(默认容量为 0),这意味着 emit 会挂起,直到下游准备好接收。这实现了天然的背压——生产者不会比消费者快太多,除非显式引入缓冲。

public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> {
    return SafeFlow(block) // SafeFlow 继承 AbstractFlow,最终调用 collect 时启动协程执行 block
}

FlowCollectoremit 方法是一个挂起函数,在 SafeFlow 的实现中,它会调用 collectoremit,而 collector 最终会调用消费者的 onEach 等操作。整个链条都是挂起的,数据通过挂起传递,不涉及线程阻塞。


⚙️ 操作符的实现原理

Flow 的操作符绝大多数都是通过构建新的 Flow 来实现的。例如 map 操作符:

public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = flow {
    collect { value ->
        emit(transform(value))
    }
}

它内部创建了一个新的 Flow,在新的 Flow 的 collect 中,它会调用原始 Flow 的 collect,对每个值应用 transform 后再 emit 出去。这种组合方式使得操作符是惰性的,只有最终 collect 时才会依次执行。

背压处理操作符的原理

  • buffer:在生产者与消费者之间插入一个带有缓冲区的 Channel。flow.buffer(10) 会创建一个新的 Flow,它启动一个单独的协程来收集上游数据并放入缓冲区,而下游的收集则从这个缓冲区读取。这通过 channelFlow 实现,本质上是开启了并发。
  • conflate:相当于 buffer(Channel.CONFLATED),缓冲区只保留最新值。
  • collectLatest:当新值到来时,如果正在处理旧值,会取消旧值的处理协程。它通过 coroutineScopeJob 的取消来实现。

🔥 热流:StateFlow 与 SharedFlow 的深入理解

StateFlow

StateFlow 是一个状态容器式的热流,它始终持有当前值,并且只向订阅者发送更新后的值。它的核心特性:

  • 去重:默认情况下,只有值发生变化(通过 equals 比较)时才会发送给订阅者。这通过内部的 updateState 机制实现,每次更新时检查新旧值是否相等。
  • 与 LiveData 对比StateFlow 是纯 Kotlin 的,不依赖 Android 主线程,完全基于协程,更适合跨平台项目。但它不处理生命周期(需要结合 repeatOnLifecycle)。
  • 实现原理MutableStateFlow 内部维护一个 value 变量和一个版本号。当 value 更新时,它会通知所有订阅者(通过 collect 的挂起函数恢复)。订阅者通过 collect 挂起,并在值变化时被唤醒。

SharedFlow

SharedFlow 是一个事件广播式的热流,它不持有状态,只负责将事件发送给当前订阅的收集者。它的核心特性:

  • 重放缓存:可以配置 replay 参数,让新订阅者收到之前发送过的若干条事件。
  • 溢出策略:可以配置缓冲区大小和溢出策略(SUSPENDDROP_OLDESTDROP_LATEST)。
  • 实现原理SharedFlow 内部维护一个循环缓冲区(ArrayDeque)存储事件,以及一个订阅者列表。当事件 emit 时,会遍历所有订阅者并恢复其挂起的 collect 协程。shareIn 操作符可以将冷流转换为热流,它会在指定的协程作用域内启动一个收集协程,并将收集到的事件广播给所有订阅者。

🧪 Flow 的异常处理与取消

异常处理

Flow 的异常处理主要通过 catch 操作符。catch 只捕获上游的异常,不捕获下游(collect 内)的异常。它的原理是:当上游抛出异常时,异常会沿着 collect 链传播,直到遇到 catch 操作符。catch 内部可以重新 emit 数据或重新抛出异常。例如:

flow {
    emit(1)
    throw RuntimeException("error")
}.catch { e ->
    emit(-1) // 发送一个默认值
}.collect { println(it) } // 输出 1, -1

如果 collect 内部抛出异常,只能在 collect 外部用 try-catch 捕获。

取消

Flow 的取消遵循协程的结构化并发规则。当 collect 的协程被取消时,Flow 的所有挂起点(如 emitdelayflow 内部的挂起函数)都会抛出 CancellationException,从而终止整个流。例如:

val job = launch {
    flow {
        repeat(100) {
            delay(100)
            emit(it)
        }
    }.collect { println(it) }
}
delay(500)
job.cancel() // 流会在下一次 emit 或 delay 时取消

📊 Flow 与 RxJava 的对比

特性FlowRxJava
背压策略默认同步,可用 buffer 等操作符开启缓冲需要显式选择背压策略(如 BackpressureStrategy
线程调度flowOn 切换上游线程,下游线程由 collect 所在协程决定subscribeOnobserveOn 分别指定订阅和观察线程
取消机制基于协程取消,自动传播需要手动管理 Disposable
操作符数量丰富但少于 RxJava非常丰富,几乎涵盖所有需求
学习曲线平缓,熟悉协程即可上手陡峭,需要理解多个概念
与 Android 集成需要 repeatOnLifecycle 处理生命周期需要 RxLifecycle 等库

Flow 更适合与协程配合的新项目,而 RxJava 在遗留项目中仍占有一席之地。


🏗️ Flow 在架构中的典型使用模式

1. 数据层:返回 Flow

数据层(Repository)通常提供 Flow<T> 作为数据源,例如 Room 数据库的查询会自动返回 Flow,网络请求也可以包装成 Flow:

fun getUserStream(userId: String): Flow<User> = flow {
    while(true) {
        val user = api.fetchUser(userId) // 轮询
        emit(user)
        delay(5000)
    }
}.flowOn(Dispatchers.IO)

2. 领域层:使用 Flow 进行业务转换

UseCase 或 Interactor 可以对数据层的 Flow 应用操作符,进行数据转换、合并、过滤等,然后再提供给 UI 层。

3. UI 层:收集 Flow

在 Android 中,UI 层收集 Flow 时需要考虑生命周期安全。推荐使用 repeatOnLifecycle

class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    // 更新 UI
                }
            }
        }
    }
}

在 Compose 中,可以直接使用 collectAsStateWithLifecycle()


🧠 深入理解:Flow 的冷热转换与作用域

冷流转热流

有时我们需要将冷流转换为热流,以便多个订阅者共享同一份数据,或者让数据在后台持续产生。Hilt 提供了 shareInstateIn 操作符。

  • shareIn:将冷流转换为 SharedFlow。需要指定一个协程作用域,在该作用域内启动一个收集上游的协程,并将收集到的事件广播给所有订阅者。
  • stateIn:将冷流转换为 StateFlow。同样需要作用域,并且可以设置初始值。
val sharedFlow = someFlow.shareIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    replay = 1
)

SharingStarted 策略控制着共享的开始和停止时间,WhileSubscribed 表示当有订阅者时开始,当最后一个订阅者消失后延迟一段时间停止。

Flow 与协程作用域

Flow 本身不持有作用域,它的执行依赖于调用 collect 的协程作用域。因此,在 viewModelScope 中启动的 collect 会跟随 ViewModel 的生命周期,而在 lifecycleScope 中启动的会跟随 Activity/Fragment 的生命周期。这种设计使得 Flow 非常安全,不会发生泄漏。


🛠️ 常见陷阱与最佳实践

  1. 不要在 flow { } 中使用 launchasync:因为这样会开启新的协程,但这些协程的生命周期不受 Flow 控制,可能导致泄漏或数据竞争。应该使用 callbackFlow 来处理基于回调的异步 API。
  2. 正确处理异常catch 只能捕获上游异常,collect 内的异常需要在 collect 外捕获,或者使用 onEach 进行预处理。
  3. 避免在操作符中执行耗时操作:耗时操作应该放在 flowOn 指定的调度器中,或者通过 map 调用挂起函数(但注意挂起函数会阻塞流,除非使用 buffer 开启并发)。
  4. 合理选择热流策略:UI 状态用 StateFlow,一次性事件用 SharedFlow,不要混用。
  5. 注意流的取消:如果 Flow 内部有无限循环(如轮询),确保在不再需要时能通过取消协程来停止。

📖 总结

Flow 是 Kotlin 协程体系中处理异步数据流的终极武器。它的设计兼顾了简洁性、强大性和安全性,通过挂起函数实现了天然的背压,通过操作符提供了丰富的组合能力,通过冷热流的区分满足了不同场景的需求。深入理解 Flow 的原理,不仅能让我们写出更优雅的代码,还能避免常见的陷阱。