用途
本页面是一个 Kotlin 缺陷模式(footgun)的讲解条目,主题是 MutableStateFlow 的合并(conflation)语义如何让“写入顺序”变成正确性问题,从而在状态持有者(state holder)中把状态“写反”。它适合用作代码评审、缺陷排查和编写测试时的参考案例。
主要能力(讲解内容)
- 机制解释:MutableStateFlow 只持有一个值;编译后的类里只有一个 volatile 状态字段和一个序列计数器,没有每次写入的缓冲区;收集器唤醒时只读到槽位里的当前值,中间的写入直接丢失。
- 失败案例:对一个回调
onIsLoadingChanged(isLoading),代码先无条件写入 Loading,再在某些路径写入 Ready,最后在else分支调用stopBufferedUpdate(),而该取消函数内部又写入 Loading,且最后执行。于是传入 false 时最终停在 Loading,传入 true 时可能停在 Ready,两个分支发布的状态都与事件相反。 - 修复范式:每个事件只写一次,在末尾统一决定;并且写入值必须与参数含义一致。示例中根据 bufferedPosition 与 currentPosition 比较,决定写 Ready 还是 Loading。
- 排查陷阱:
- 名为 stopX 的辅助函数也可能发布状态;应先用
grep -rn "_state.value\|_state.update" --include="*.kt" src/枚举所有写入点,按所在函数分组阅读,找出一个事件内被到达两次的函数,或两次写入之间没有挂起点的情况。 - 周期性发射器若在重启前没有取消,会泄漏多个循环反复推送同一值,最终“投票压过”单次写入的修复;示例中的进度循环和缓冲循环都以
job?.cancel()开头。 - 合并并不保证中间值一定被丢弃,是否可见取决于调度;同线程测试收集器往往能看到每一次写入,因此顺序错误可能在测试中通过、在设备上复现。
- 比较两个值前要统一单位:把缓冲百分比乘以时长再与毫秒位置比较,会差两个数量级。
- 消费端可能二次反转:终结分支把位置停在负哨兵值,而格式化器把所有负值渲染为占位符,就会让占位符留在屏幕上;应停在真实值上,例如
Ended -> _timeline.update { it.copy(current = it.total.coerceAtLeast(0L), loading = false) }。 - 多条触发路径(如 load-changed 回调与 playback-state 回调)都需要遵守同一“单次写入”纪律,否则另一条路径仍会覆盖。
- 名为 stopX 的辅助函数也可能发布状态;应先用
使用方式
页面未提供安装命令或代码集成步骤,属于知识讲解型条目。可按文中建议的做法使用:用 grep 命令枚举写入点、在取消函数处注明“有意不发射”、用收集器逐条记录发射日志来验证,而不是在写入方回读状态字段。
适用场景
适用于使用 Kotlin 协程与 MutableStateFlow 编写播放器、加载状态、进度轮询等状态持有者的开发与代码评审场景,尤其是同一回调内多次写状态、存在命名疑似无副作用的辅助函数、或存在周期性发射器的代码。
限制
页面未提供该条目的平台要求、版本兼容性、安装方式、收费信息或量化效果数据;所给代码为改编示例,并非可直接套用的完整实现。合并行为的具体表现依赖运行时调度,测试中不保证复现。