关于 state-and-background-footguns
面向 Kotlin 和 Compose Multiplatform 开发者的技能,整理了从生产应用中提炼的 23 个响应式状态与后台任务陷阱。各参考文件介绍可行模式、故障原因及在代码库中验证的方法。
主要功能
- 0123 个专题参考文件
- 02响应式状态与 Flow 模式
- 03后台任务和服务指南
- 04说明故障模式及原因
- 05提供代码库验证命令
适用场景
- 01诊断过时或不一致的界面状态
- 02避免后台通知重复或遗漏
- 03检查持久化与备份行为
- 04强化桌面应用启动与运行时
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ maxrave-dev/ kotlin-footguns --skill state-and-background-footguns安装后在 Claude Code 中输入/state-and-background-footguns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/app-backup-to-zip-mediastore.mdcombine-two-flags-to-gate.mdcompose-desktop-runtime-hardening.mdcompose-multiplatform-viewmodel-base.mddatastore-driven-workmanager.mdderive-the-flag-dont-store-and-correct-it.mddesktop-single-instance-before-di.mddistinct-by-key-reset-cancel-per-item.mddual-mode-persist-blocking-or-async.mdflatmaplatest-resubscribe-composite-key.mdglance-layout-vocabulary.mdglance-widget-over-existing-state.mdkmp-logger-facade.mdnamed-job-lifecycle-discipline.mdon-demand-dictionary-asset.mdone-snapshot-per-period-not-many-flows.mdpending-state-makes-waiting-legible.mdperiodic-worker-dedup.mdpreset-identity-read-back-from-value.mdreadonly-stateflow-cold-to-hot.mdremoteviews-bitmap-budget.mdstateflow-conflation-inverts-state.mdswappable-crash-reporting-and-dialog.md详细介绍
响应式状态与后台工作中的易错点
从一个生产环境的 Kotlin 和 Compose Multiplatform 应用中总结的 23 个陷阱,每个陷阱都放在 references/ 下的一个短文件中。每个文件都介绍可行模式、陷阱(实际使用中出错的具体方式及原因),以及一个验证方法部分,列出可针对用户自己的代码树运行的命令。
如何使用此索引。 将即将编写的代码或正在排查的症状与下方条目对照:每个条目都标明主题,以及应该促使你查阅该条目的症状。提出代码或修复方案之前,请使用 Read 工具阅读所有可能适用的文件;每个文件都不到 150 行。如果验证命令中提到了源项目的路径,请替换为用户代码树中的对应路径。
交叉引用。 如果文件中有反引号括起来的陷阱名称,但未在此处列出,则该名称位于同级区域的技能中:使用 glob ../*/references/<name>.md 打开它。
响应式状态:Flow、StateFlow 和 ViewModel
stateflow-conflation-inverts-state— 合并状态持有器只有一个槽位,因此回调在每个事件中多次写入时,写入顺序就成了正确性的关键——收集器只能看到最后一次写入。适用于加载内容上方出现旋转加载指示器、加载标志在错误的时机清除,或屏幕显示的是上一步的状态等情况。named-job-lifecycle-discipline— 每个关注点使用一个var xJob: Job?字段,将每个启动位置的“先取消再重新启动”作为不变量,并将拆除时的写入包装起来,避免取消导致写入中途停止。适用于周期性更新在每个计时周期内到达多次、先前项目的过期结果覆盖当前项目,或长生命周期对象在释放后仍继续工作等情况。readonly-stateflow-cold-to-hot— 通过asStateFlow()暴露状态,而不是将可变持有器向上转型;使用stateIn(scope, WhileSubscribed(timeout), initial)将冷源 Flow 转为热流,使其在收集器频繁变化时仍能存续,而不会永远运行。适用于屏幕在每次旋转或导航时重新查询数据源、最后一个收集器离开后工作仍继续,或所有者之外的对象写入本应只读的状态等情况。flatmaplatest-resubscribe-composite-key— 使用 flatMapLatest 在键 Flow 变化时重新订阅下游 Flow,再通过 distinctUntilChanged 将产生的高频流归并为复合键,然后触发开销较大的一次性工作。适用于每次进度更新都会重新获取数据、快速切换后为错误项目触发获取,或某个输入最后到达导致副作用始终不执行等情况。distinct-by-key-reset-cancel-per-item— 按项目 ID 使用 distinctUntilChangedBy 对每个项目建立处理流水线,在启动下一个项目之前取消前一个项目尚在进行的工作,并在填充状态前重置可见状态——这样前一个项目的任何内容都不会显示在新项目下。适用于详情屏幕短暂显示上一个项目的图片或文本、较慢的响应覆盖较新的响应,或切换到某个字段没有值的项目后该字段仍保留旧内容等情况。combine-two-flags-to-gate— 使用combine+distinctUntilChanged+collectLatest将多个独立条件 Flow 转换为一个开关门控,使启动和拆除分支都具备幂等性,并以不可取消的方式运行拆除流程。适用于子系统在完全配置前就启动、某个前置条件被撤销后仍继续运行,或快速切换后陷入半启动状态等情况。compose-multiplatform-viewmodel-base— 适用于 Compose Multiplatform 的共享 ViewModel 基类——能够感知容器,让子类无需通过构造函数传递依赖即可获取额外依赖,并为每个屏幕提供统一的加载和错误界面——以及此类基类几乎总会出现的阻塞式资源查找隐患。适用于每个屏幕都在重复实现自己的加载对话框、基类需要某些子类才使用的依赖,或应用启动时在主线程上卡顿、尚未绘制任何内容等情况。dual-mode-persist-blocking-or-async— 同一个挂起持久化函数体可通过两种方式驱动——关闭路径上采用阻塞方式,确保写入在作用域销毁前完成;周期性计时中则采用即发即弃方式——避免两份副本逐渐偏离。适用于应用运行期间保存的状态正确,但强制退出后不正确、拆除时的保存操作悄无声息地失效,或两个保存函数的保护条件已经不同等情况。pending-state-makes-waiting-legible— 将“已请求,但尚未收到答复”建模为独立字段,因为没有它,被拒绝的请求和对方尚未查看屏幕的情况就无法区分——两者看起来都像什么也没发生。涵盖单个请求对应多个清除操作、不会收到答复时在本地清除,以及为用户提供退出等待的方式。适用于操作看起来毫无反应、屏幕可能永远卡在等待状态,或错误消息无处显示等情况。preset-identity-read-back-from-value— 通过将预设与当前生效的值进行比较来推导“这是哪个预设”,而不是在旁边存储一个标签,这样编辑后会自动变为“自定义”,恢复原值时则会重新选中;对于由预设自身数值决定的每个预设字段,也应直接推导,而不是逐行写入。适用于预设选择器持续显示过期名称、预设从不自动重新选中,或二十行中的一行常量已经偏离等情况。one-snapshot-per-period-not-many-flows— 通过一次挂起调用,以单个不可变快照返回整个周期的各项数据,而不是返回十几个独立 Flow、让屏幕自行协调——比较两个时间段的屏幕需要确保每个时间段的数据一致,分开的数据发射可能导致本周期的计数与上个周期的总计并排显示。涵盖将单次发射的 Flow 写成挂起函数的情形、哪些派生数据应纳入快照,以及比率的分母应采用人们所理解的含义。适用于比较屏幕在重新加载时短暂显示不匹配的数字、一个屏幕的资源库中即将加入第十个 Flow,或两个位置以不同方式计算同一个平均值等情况。derive-the-flag-dont-store-and-correct-it— 作为已收集状态的纯函数的布尔值仍被单独存储为mutableStateOf,并用一个猜测值初始化,再由LaunchedEffect在一帧后修正——因此首帧会呈现猜测值,而一旦rememberSaveable已保存旧值,仅修改种子值也不会产生效果。适用于 UI 元素在冷启动时明显地闪现“显示—隐藏—再显示”,或修改remember/rememberSaveable初始值却无法改变已运行应用当前显示内容等情况。
后台工作、服务与平台运行时
app-backup-to-zip-mediastore— 将应用备份为一个 zip 文件,保存到用户自己的 Downloads 文件夹中,无需存储权限,并且只保留最新的 N 个归档文件。涵盖在复制文件前检查点数据库的预写日志、先在缓存中组装归档文件、通过系统媒体存储插入文件,以及通过查询同一集合清理旧归档文件。仅限 Android。适用于恢复的备份缺少最近写入的数据、备份文件在用户的文件管理器中不可见,或旧备份一直累积的情况。datastore-driven-workmanager— 直接根据设置存储驱动定期后台工作,让开关或间隔变更立即生效,无需重启。涵盖将多个偏好设置流合并为一个调度决策、使用原位更新的入队策略使间隔变更真正生效、停用时按唯一名称取消任务,以及为什么工作器必须自行重新读取相同设置。仅限 Android。适用于更改备份或同步间隔后,必须重新安装才会生效;用户关闭功能后任务仍在运行;或多个调度任务发生叠加的情况。periodic-worker-dedup— 构建周期性通知器,即使运行延迟也不会漏掉项目,并且只有在一个很短的时间窗口内被终止,才会重复处理同一项目。涵盖将“已处理”记录保存在数据库中,而不是工作器中;有意扫描一个比调度间隔更长的时间窗口,以便跳过的运行能够补上;以及按从旧到新的顺序处理,使被中断的运行丢失最新项目,而不是随机丢失一批项目。仅限 Android。适用于用户反映设备重启后出现重复通知、设备闲置时漏掉项目,或首次运行时将整个历史项目库一次性推送给用户的情况。desktop-single-instance-before-di— 安排桌面应用的启动顺序,让单实例保护在依赖容器启动和任何磁盘状态打开之前运行。涵盖将第二次启动的参数转发给正在运行的实例并退出、将来自 UI 框架外部的恢复请求转交给活动窗口,以及第二次启动根本不会生成第二个进程的平台。适用于第二次启动应用时发生崩溃或设置损坏、应用运行时打开链接没有任何反应,或第二个窗口抢占第一个窗口所持有文件的情况。compose-desktop-runtime-hardening— Compose 桌面入口点在首个窗口创建之前必须按顺序执行的探测、系统属性设置和平台检查。涵盖在加载任何原生库之前预热 JDK 的桌面集成 API、仅在启动时读取的渲染器和互操作属性、在等待可能阻塞 UI 线程时关闭垂直同步、通过虚拟机检测控制透明效果和自定义标题栏,以及通过反射设置 Linux 窗口类名称以便桌面入口正确绑定。适用于将窗口移到另一台显示器后 UI 冻结但音频仍在播放、窗口在虚拟机中始终不出现,或 Linux 程序坞显示的是类名称而非应用名称的情况。swappable-crash-reporting-and-dialog— 通过替换一个模块,从同一代码库构建包含崩溃报告功能和完全不含该功能的版本。该模块公开三个顶层函数而非接口,因此各调用点的字节码完全相同,并能证明无跟踪版本中不含任何报告代码。还涵盖使用较旧的窗口小部件工具包构建桌面崩溃对话框,因为现代 UI 可能正是刚刚崩溃的部分,以及如何将其调度到该工具包的事件线程上。适用于隐私版本必须完全不包含报告依赖、替换后的实现与对应实现逐渐偏离,或应用崩溃时没有可见错误且用户无法向你发送详情的情况。kmp-logger-facade— 在共享模块中放置一个小型日志对象,作为所有调用点与日志库之间的中介,这样就能用一行代码静默某个嘈杂的子系统,也能在不修改调用点的情况下替换日志库。涵盖静音标签集合、供调用方在运行时选择严重级别的枚举,以及为什么在任何地方直接导入一次该库就会让这两项优势都失效。适用于某个子系统的日志淹没其他内容、替换或升级日志库需要编辑数百个文件,或静音某个标签对其部分输出不起作用的情况。glance-widget-over-existing-state— 构建一个渲染应用现有状态持有者而非并行副本的主屏幕小组件:注入应用已在使用的同一个共享状态对象和同一个长期作用域,并在状态变化时重新发出小组件更新。涵盖从小组件按钮分发应用自身的 UI 事件、为什么注入目标必须是单例而非屏幕作用域定义、让整个小组件都可点击、关闭小组件必须读取的图片所用的硬件位图,以及启动这些收集器时要避免在小组件的 provide-glance 回调中造成泄漏。仅限 Android。适用于小组件显示过时的播放或会话状态、图片为空、点击小组件后打开启动器菜单或毫无反应,或小组件按钮需要各自实现重复逻辑的情况。remoteviews-bitmap-budget— 主屏幕小组件绘制的每张位图都会被复制到跨进程传递的 RemoteViews 载荷中,而平台会拒绝位图总量超过固定预算的更新。因此,按绘制尺寸解码每张图片并非一种优化,而是让小组件持续显示的必要条件。涵盖决定可以容纳多少张图片的像素计算、为什么系统日志之外看不到该故障,以及为什么读取同一图片的每个界面都必须对缓存键和解码尺寸达成一致。仅限 Android。适用于小组件显示框架的错误占位图、在一台设备上能正常渲染而在另一台上不行,或多加一个磁贴就导致整个小组件消失的情况。glance-layout-vocabulary— 可组合小组件工具包并不是换了一个导入项的 Compose:它没有纵横比,权重始终为 1,因此均分就是全部布局能力;其圆角半径仅适用于 API 31;而且小组件始终填满启动器单元格,因此必须有意利用多余高度。涵盖保持方形磁贴为正方形的加权间隔区技巧,以及填充修饰符会吞掉整个区域的情况。仅限 Android。适用于方形磁贴渲染为长方形、小组件内容下方出现一大块纯色空白,或同一组件在一台设备上圆角而在另一台设备上为直角的情况。on-demand-dictionary-asset— 分词器、拼写检查器或分析器需要一个数 MB 的词典,但该功能大多数用户都不会启用,随应用安装词典会让所有安装包变大。应仅在用户选择启用时下载一次到普通目录,并让每个使用方都向文件系统查询“是否就绪”,而不是相信某个标记。适用于某种语言的资源只会让一个目标平台的安装包变大、半下载的资源绝不能被视为已安装,或功能在下载报告成功后仍无法正常工作的情况。
评论
支持 Markdown