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 页面状态设计时很稳的起点。

参考资料与延伸阅读

相关推荐
SHENGWEI胜为1 小时前
局域网出现 IP 地址冲突怎么办?如何从 ARP 表定位故障设备
网络协议·tcp/ip·php
TimeFine4 小时前
AI 编码工作流的设计
android·ai编程
骑着蜗牛撵大象3274 小时前
C++20 协程 × 屏障:手写一个 co_await 版 Barrier,让等待不再钉死线程
android·c++20·协程·并发编程·co_await·屏障同步
4SAPI4 小时前
多模态 AI 应用统一接入网关生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
大数据·人工智能·架构·php·1024程序员节
4SAPI5 小时前
营销内容生成大模型 API 生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
数据库·人工智能·架构·php
山东孙燕姿6 小时前
设备台账重构:从一张表到三层模型
android·java·网络·低代码·重构·rxjava
迪康Defender6 小时前
PHP 高危漏洞应急分析:从风险原理到端点侧纵深防护
运维·开发语言·安全·自动化·php·运维开发·远程工作
躺柒7 小时前
读数据安全架构设计与实践16安全架构案例(上)
安全·php·零信任·安全架构·防火墙·无边界
蒸鱼Yuzheng7 小时前
移动端 CPU 调度分析:线程亲和性、大小核迁移与卡顿
android·perfetto·性能分析·线程优化·cpu调度
booboosui7 小时前
安卓唤醒词:12 维 MFCC 加 DTW,阈值 1.8
android