Kotlin Flow、StateFlow、SharedFlow 全解析:冷流、热流与 Android 状态管理

同一个页面旋转后又请求了一次网络?新打开的界面立刻看到了旧状态?发送一次事件却没有任何 UI 收到?这些问题往往不是某个 collect 写错了,而是没有先想清楚:数据什么时候开始生产、多个订阅者是否共享生产、后来订阅的人应该拿到什么。

本文从普通 Flow 的冷流行为出发,比较 StateFlow 与 SharedFlow,再落到 ViewModel 与页面生命周期。一次性导航/Toast 事件的完整选型会在下一篇单独展开。

一、普通 Flow:每次收集通常重新执行上游

kotlin 复制代码
val numbers = flow {
    println("start")
    emit(1)
    emit(2)
}

numbers.collect { println(it) }
numbers.collect { println(it) }

这里 start 会打印两次。flow {} 定义的是一段生产过程;通常创建 Flow 不会立刻执行上游,新的 collect 会重新运行这段过程。如果上游是网络请求,两位收集者可能触发两次请求。

"冷"不等于"自动在后台"。flow {} 默认遵循收集方上下文;需要安排上游执行环境时可用 flowOn,但真正的非阻塞还取决于底层 API。别把阻塞 I/O 原样放到主线程 Flow 中。

二、StateFlow:永远有一个当前状态

kotlin 复制代码
data class UiState(val loading: Boolean, val title: String)

private val _uiState = MutableStateFlow(UiState(loading = true, title = ""))
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

fun showTitle(value: String) {
    _uiState.update { current -> current.copy(loading = false, title = value) }
}

StateFlow 是热流,有必需的初始值,能通过 value 读取当前状态。新订阅者会先看到当前最新状态,而不是从应用启动那刻重播所有中间变化。这很适合页面上的"现在是什么":加载中、内容、错误、选择项。

StateFlow 基于相等性合并相同状态;并发更新时用 update 等原子更新方式,避免 _uiState.value = _uiState.value.copy(...) 这类先读后写在并发下丢失修改。即便更新了多个不同值,慢订阅者也可能只看到较新的值,不要把每一个支付或审计事件都当作 StateFlow 状态变化来可靠传送。

三、SharedFlow:可配置的热流广播

kotlin 复制代码
private val _announcements = MutableSharedFlow<String>(replay = 0)
val announcements: SharedFlow<String> = _announcements.asSharedFlow()

suspend fun publish(text: String) {
    _announcements.emit(text)
}

SharedFlow 也已经存在于收集者之外,可向多个当前收集者广播值;它不要求初始值。replay 决定新订阅者会收到多少个历史值,默认是 0。缓冲容量与溢出策略可以影响慢订阅者,但**replay = 0 时,没有订阅者期间发出的值不会被以后来的订阅者补领**。即使配置了 extraBufferCapacity,无人订阅时也不要误以为它能替你永久存事件。

这意味着 SharedFlow 适合广播"当前在听的人需要知道"的消息;需要跨页面重建、进程死亡或离线重试的业务结果,不能仅依赖内存中的广播。

四、三者放在一起比较

维度 普通 Flow StateFlow SharedFlow
冷/热 通常冷 热 热
订阅前是否需要初始值 不需要 必须有 不需要
新订阅者看到什么 通常重新执行上游 当前最新状态 replay 中保留的值;默认没有
多个订阅者是否共享生产 默认不共享 共享当前状态 共享广播源
常见用途 数据查询/转换管线 UI 当前状态 多订阅者通知与事件广播

这不是"哪个 API 更高级"的排序。先确定业务要的是过程 、当前值 ,还是广播;再选类型。

stateIn 与 shareIn:把冷上游变为共享热流

kotlin 复制代码
val sharedState = repository.observeItems()
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = emptyList()
    )

stateIn 使多个 UI 收集者共享上游,并保留一个当前值;shareIn 则产生按 replay 配置的 SharedFlow。关键参数不只是 replay,还有谁持有 Scope、何时启动、没人订阅后是否停止 。SharingStarted.Eagerly、Lazily、WhileSubscribed 对资源消耗和重新订阅行为不同,不能复制一份配置到所有数据源。

上例的 5_000 是停止上游前的宽限时间,常用于短暂旋转重建时避免立刻停止又启动。它不是通用最佳值;网络轮询、数据库观察与页面状态的成本不同,应按实际生命周期选。

五、算子位置决定你能捕获和切换什么

kotlin 复制代码
repository.observeItems()
    .map { items -> expensiveTransform(items) }
    .flowOn(Dispatchers.Default)
    .catch { error -> emit(emptyList()) }
    .collect { items -> render(items) }

flowOn 改变其上游 的执行上下文,下面的 collect 不会因此自动跑到 Default。catch 捕获其上游 流操作的异常,不能兜住后续 render(items) 抛出的异常。协程取消也不应被当成普通数据错误吞掉。

如果 expensiveTransform 是 CPU 密集工作,Default 可能合适;若上游底层已经是异步数据库/网络 API,不要机械地再套一层 IO。先确认具体瓶颈和线程行为。

六、生产快、消费慢:buffer、conflate、collectLatest

算子 发生什么 适合什么
buffer 在缓冲范围内让生产与消费重叠 消费偶尔慢,但值仍应逐个处理
conflate 消费跟不上时跳过中间值 只关心较新的进度/状态
collectLatest 新值到来时取消上一次处理 搜索建议、只展示最新结果

例如搜索框文本变化后加载建议,旧查询的结果不再有展示价值,可以在合适边界使用 collectLatest。支付流水、上传确认和审计日志不能随意跳过中间值。背压策略必须由业务语义决定。

七、Android 页面怎样收集才不重复工作?

Fragment 中优先按视图生命周期收集:

kotlin 复制代码
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> render(state) }
    }
}

低于 STARTED 时,repeatOnLifecycle 会取消内部收集块;重新进入时再启动。若上游是冷流,会重新执行;若 ViewModel 使用 stateIn 共享,上游行为取决于其启动策略。Compose 页面可用 collectAsStateWithLifecycle() 把 Flow 转为生命周期感知的 State。不要在每次 onResume() 或每次重组时无管理地新开收集器。

八、面试高频问答

Q1:flow {} 为什么叫冷流?

通常没有收集者就不生产;每次新的收集会重新执行上游。

Q2:StateFlow 和 SharedFlow(replay = 1) 一样吗?

不一样。StateFlow 必须有初始值、可通过 value 读当前状态,并基于相等性合并;普通 SharedFlow 的初始值、重放和缓冲语义由配置决定。

Q3:SharedFlow(extraBufferCapacity = 1, replay = 0) 会在无人订阅时保留一个事件吗?

不会;无人订阅时只保留 replay 指定的历史,额外缓冲不能当离线邮箱。

Q4:flowOn 会把 collect 切到后台吗?

不会,它影响上游;收集逻辑使用收集方上下文。

Q5:上游 catch 能接住 collect {} 的异常吗?

不能。算子只捕获位于它上游的流异常。

Q6:为什么页面旋转后重复请求?

可能每次都重新收集冷上游;检查收集位置、共享 Scope 与启动策略,而不是只在界面加布尔开关。

Q7:StateFlow 适合保存一次性导航事件吗?

通常要谨慎。新订阅者会重新看到当前值,可能重复执行导航;先定义事件能否丢失、是否要跨重建保留,再选模型。

一句话总结:**Flow 描述过程,StateFlow 表达现在,SharedFlow 广播给订阅者。**这不是绝对的 API 限制,却是做 Android 页面状态设计时很稳的起点。

参考资料与延伸阅读

相关推荐
应用市场40 分钟前
把旧 Pixel 变成相册备份中转站(上):Mac 到安卓的照片传输工具设计——流式上传、sha256 校验、adb forward 与 Bonjour
android·macos·adb·kotlin·swift
汤米粥1 小时前
后端开发主流技术方案
java·python·golang·php·nodejs·后端开发
墨天梦1 小时前
B08_Fragment事务通信与双生命周期
android
Kapaseker1 小时前
Android 以后可能不会再有横竖屏适配了
android·kotlin
在猴站学算法2 小时前
(实战)PHP文件包含漏洞
网络安全·php
墨天梦2 小时前
B13_通知图片相机与影音
android·数码相机·kotlin
事圆则缓2 小时前
Android 一次性事件怎么设计:Channel、SharedFlow 还是 UI State?
android·ui
谢亮_vipxieliang2 小时前
PHP 8 新特性:构造器属性提升与实战
开发语言·后端·php
Highcharts.js3 小时前
动态数据可视化架构:Highcharts + PHP + MySQL 从数据库到数据渲染
数据库·mysql·php·开发文档·实时数据·highcharts·图表开发