SuperAPI AI 工具集

readonly-stateflow-cold-to-hot

编程与开发

查看项目

更新于 10-17 16:16

技能介绍

本技能针对 Kotlin 协程中 StateFlow 的两个常见错误,每个错误都只需改一行代码,但错误写法往往能编译、也“基本能用”。

一、只读暴露 状态持有者只有一个写入方,其余都是读取方,因此公开类型应当是只读的。推荐写法是用 asStateFlow(): private val _controlState = MutableStateFlow(ControlState(...)) val controlState: StateFlow<ControlState> = _controlState.asStateFlow() 资料指出,向上转型(val x: StateFlow<T> = _x)和 asStateFlow() 并不等价:反编译后可以看到,asStateFlow(m) 会新建一个 ReadonlyStateFlow 包装 m,它不是 MutableStateFlow,也无法被强转为 MutableStateFlow;而向上转型只是把同一个对象换成更窄的静态类型交出去,持有者仍可强转回去写入,例如: (viewModel.someState as MutableStateFlow<Boolean>).value = true // compiles, and works 可以用 grep 找出没有调用 asStateFlow 的赋值: grep -rn "StateFlow<.> = _" --include=".kt" src/ | grep -v asStateFlow 资料认为,对于严格模块内部的持有者,向上转型尚可接受;但只要其他模块能访问到,它就不合适。 另一种写法 val x: StateFlow<T> get() = _x 会语义不同:每次读取都会重新求值;当 backing field 是 val 时二者等价,当它是 var 时则会误导调用方。建议每层只用一种写法。

二、冷源变热源 偏好存储、数据库查询等源流是冷的:每个收集者各自发起读取。用 stateIn 共享一次,并设置保活窗口,可让各屏幕共享同一值,并在最后一个收集者离开后稍晚停止上游: val openAppTime: StateFlow<Int> = dataStore.openAppTime.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), 0) val listPlaylists: StateFlow<List<PlaylistEntity>> = repository.getAllPlaylists() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

三、注意事项

  • WhileSubscribed 的超时值是一种策略,而不是固定常数:它决定最后一个收集者离开后上游继续存活多久,用来衔接配置变更、导航切换造成的短暂空档。太短会导致每次旋转都重读;太长会让已经离开屏幕的用户仍承担开销。资料批评两个成本完全不同的源却共用 5 秒,而另一处对内存内 MutableStateFlow 的两个 map 只花 1 秒;不宜全项目统一常量。
  • Eagerly 在构造时就启动且永不停止,只适合必须提前预热者;Lazily 在首次收集时启动后永不停止,看似修复问题,实为泄漏。
  • stateIn 需要同步提供初始值,而该初始值会被直接显示:emptyList() 显示为空列表而非加载中;若二者需要区分,应让状态类型显式包含“尚未加载”。
  • 共享会改变流的身份,因此 stateIn 必须放在属性上;放在 getter 或函数体中会在每次调用时新建共享流,起不到共享作用。
  • SharedFlow 暴露不能与 StateFlow 互换:asSharedFlow() 能编译但不再提供 .value,晚到的收集者拿到的是重放缓存而非“当前状态”。事件用 SharedFlow,状态用 StateFlow。

四、验证方式

  • 暴露:尝试把公共属性强转回 MutableStateFlow 并赋值;能运行说明是向上转型,asStateFlow() 会在运行时失败。这适合每层做一次一次性检查,不必保留为测试。
  • 共享:在冷源自身的构建器(而非收集者)中打一行订阅日志,然后离开屏幕再返回,分别快于和慢于超时:快时不应出现新订阅行,慢时应恰好出现一行;若旋转产生订阅行说明未共享,若离开后永不停止说明用了 Lazily 或 Eagerly。
  • 不要根据持有者读回的值判断,因为持有者读写同一字段,无论用哪种写法都会看到一致的值。