Flow 的问题不是流动,是谁还在听
一个 suspend 函数给一次结果;Flow 描述一段会持续变化、也会被取消的关系。
Snackbar「消息已发送」最适合建模成什么?
- 永久留在
UiState的 Boolean StateFlow<Boolean>,显示后由 UI 改回 false- 一次消费的效果信号,或从持久状态推导出的 UI 行为
- 放在 Activity 的全局变量里
冷流每次收集都重新开始
flow { } 是冷流:没有 collector 就不执行,每个 collector 都启动一份独立生产过程。Room 查询 Flow 也遵循这种订阅语义,数据库变化时重新发值。冷流适合描述「如何得到这一串值」,而不是一个已经持续存在的广播源。
StateFlow 是热的、有当前值、按相等性合并的状态容器。新订阅者立刻拿到最新状态,而不是历史全部过程。SharedFlow 是可配置重放与缓冲的广播;Channel 更像队列,一条元素通常交给一个接收者。
UI 状态不是事件垃圾桶
把导航、Toast、Snackbar 全塞进 UiState,旋转或重新订阅时可能再次触发;再加一个 consumed 回调,会引入 UI 与 ViewModel 的双向握手和竞态。优先问:这件事能否由持久状态推导?例如发送失败气泡应留在消息状态里,根本不需要瞬时 Toast。
真正的一次性界面效果可以用不重放的 SharedFlow 或 Channel,但要接受一个事实:没有订阅者时它可能丢失。如果「绝不能丢」,它就不是一次性效果,而是应该持久化的业务状态。
操作符是在声明并发政策
map 顺序变换;combine 用各上游最新值合成状态;zip 一一配对;flatMapLatest 在新查询到来时取消旧查询,适合搜索;buffer 让生产与消费解耦;conflate 丢过时中间值,只保留最新;collectLatest 新值到来时取消旧的处理。
它们不是风格差异。搜索建议该取消旧请求,消息发送却不能因为新消息到来而取消上一条。选错一个 latest,就可能把业务承诺当作过时帧丢掉。
val uiState: StateFlow<ChatUiState> = combine(
repository.observeMessages(conversationId),
repository.observeConnection(),
draft
) { messages, connection, draft ->
ChatUiState(messages, connection, draft)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ChatUiState.Loading
)
在 Compose 里用 collectAsStateWithLifecycle()。它会在生命周期不活跃时停止收集,配合 WhileSubscribed 让昂贵上游在短暂配置变更时不抖动、长时间无人订阅时又能停下。
StateFlow 保存的是当前值,不是事件历史。订阅前连续赋值,collector 只先看到最新值:
collector sees 2 collector sees 3
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val state = MutableStateFlow(0)
state.value = 1
state.value = 2
val job = launch {
state.take(2).collect { println("collector sees $it") }
}
yield()
state.value = 3
job.join()
}在 Android Studio Kotlin/JVM scratch 运行。再把它改成 MutableSharedFlow<Int>(replay = 0),在订阅前发值,观察为什么一个「绝不能丢」的业务动作不该只靠瞬时流。
聊天列表来自 Room Flow,网络连接状态来自另一条热流,草稿来自保存状态。ViewModel 用 combine 产出一个可渲染快照。网络推送本身不是 UI 状态;先写入本地真相,再由同一条 Room Flow 更新页面,前后台路径才不会分叉。
「Flow 就是更现代的 LiveData,所以全部换成 StateFlow。」冷数据管道、共享状态、广播事件与工作队列的消费语义不同。
先回答重放多少、多个订阅者各拿什么、没人订阅时是否生产、慢消费者怎么办,再选 Flow、StateFlow、SharedFlow 或 Channel。
答案是第 3 项。永久 Boolean 会在重建后重复触发;让 UI 改回 false 又引入双向竞态。能落成状态的结果就渲染状态;真正瞬时、允许无人订阅时丢失的效果才走一次性信号。
这一章的一句话
Flow 类型的差别不在 API,而在订阅、重放、丢失与共享的合同。
下一章进入 Compose:一个计数器变化时,函数明明被重新调用了,为什么屏幕上绝大多数节点既没有重建,也没有重新布局?答案藏在 Composition 的槽位身份里。