用途
该技能面向绑定“当前条目”的界面:状态流中大多数发射只是同一条目的某个字段发生变化,而真正的条目切换需要扇出为多个相互独立的查询,且每个查询都可能比请求它的条目存活更久。技能给出在收集器顶部按固定顺序执行的三步做法,避免旧条目数据残留或错误写入。
主要能力
- 使用
distinctUntilChangedBy { it.entity?.id }只在真正发生条目变化时触发流水线。 - 在
collectLatest中先取消上一个条目的在途工作,再更新当前条目状态。 - 采用“先重置、再填充”的顺序:先把新条目没有值的字段置为 null,再按从廉价到昂贵的顺序填充数据,使旧条目的数据不可达而非仅仅过期。
- 为每个逐条目子查询维护独立句柄,执行同样的“先取消、再赋值”流程。
- 在写入时保留条目 id 校验,作为与取消无关、不受时序影响的兜底保护。
使用方式
在收集器顶部按顺序执行三步:先用 distinctUntilChangedBy 以稳定标识符过滤,再在 collectLatest 中取消上一个条目的句柄并写入当前状态,最后用新的 ScreenData 对象重置各字段,随后按廉价字段优先的顺序填充。对于每个子查询,使用独立字段与句柄,在请求前先重置该切片并取消旧任务,再启动新的 viewModelScope.launch,必要时在终端操作符前加 .cancellable()。
适用场景
适用于一个界面绑定“当前条目”、条目切换会触发多个独立查询、且这些查询可能晚于条目切换返回的 Kotlin 协程场景。
限制与陷阱
- 取消一个从未被赋值的句柄是空操作,看起来却像保护;可用
grep -rn "perItemJob" --include="*.kt" src/核实句柄是否真正被赋值。 - 取消是协作式的:挂起中的网络调用在到达可取消挂起点前不会被取消,已拿到的响应仍可能在退出路径上被写入,因此写入时的 id 比较不可省略。
- 键选择错误比不选键更糟:以整个状态对象为键会失去意义,以标题或列表位置等跨条目重复的值为键会让不同条目被误判为相同。
- 可空键会让不同条目无法区分:连续多个 null 会被折叠,从一个无实体状态切换到另一个无实体状态时流水线不会运行;若“无条目”是需要界面响应的状态,应单独处理。
- 重置会清掉新条目本身已有的字段,因此重置与填充已知字段应在同一次写入中完成,否则会出现空白帧,或在合并状态持有者上发生不可预期的折叠。
- 每个子查询需要自己的句柄,因为
collectLatest只取消收集器主体,主体启动到外层作用域的工作仍会继续运行。
验证方法
以快于最慢查询返回的速度连续切换条目并停下,依次检查:可见状态与最终停留的条目一致;没有任何字段仍保留途经条目的值;日志显示中间条目的查询被取消或在 id 校验处被丢弃。再把最慢查询人为变慢(例如在仓库中加延迟)重复验证;若最终界面填入了途经条目的数据,说明写入时的 id 复查缺失。