用途
该技能用于规范 Expo / React Native 应用中数据屏幕(加载、列表、筛选类界面)的状态呈现与用户反馈方式。它把「屏幕当前显示什么」收敛为一个单一状态联合,并把提示、横幅、确认、撤销等反馈整理成一套有层级的规则,避免加载/错误双标志、默认空数组、刷新清屏、弹窗报结果等常见问题。
主要能力
- 单一状态联合:每个数据屏幕在模型 hook 中通过
listScreenStateOf把事实(items 或 undefined、fetching、paused、失败 kind、过滤器)转成一个ListScreenState,视图只对该联合做 switch,不再维护 loading/error 标志对,也不会把查询数据默认成[]。 - 区分四种「非内容」屏幕:空、筛选后为空、失败、离线各有自己的文案和下一步操作,并有字面量 testID
<id>.state.<state>;失败文案取自失败 kind,而非error.message。 - 延迟且内容形状的加载占位:前 300 ms 不展示任何东西,之后显示与内容形状一致的高亮块,展示后至少保留 500 ms(
DelayedPlaceholder、PLACEHOLDER_TIMING),不使用整屏 spinner。 - 刷新不清屏:占位只来自「尚未加载任何内容」,后台重新拉取最多显示一行安静的更新提示;下拉刷新指示器只响应用户本人的下拉。
- 重试保留现场:Try again 重新拉取,而筛选、搜索文本、已输入内容和滚动位置保存在查询之外。
- 进行中动作反馈:
useBusyAction禁用控件、显示忙碌态并忽略第二次按压;动作失败以内联或 notice 呈现,不使用覆盖全屏的 modal spinner。 - 反馈阶梯:内联(字段/行)→ notice(已完成动作或已恢复的失败)→ banner(持续状态,如离线)→ dialog(仅不可逆决策),禁止
Alert.alert。 - 一次一个 notice:替换而非排队;结果 4 秒、带动作 8 秒,并遵循 Android 无障碍超时(
getRecommendedTimeoutMillis);屏幕阅读器运行时带动作的 notice 保留 Dismiss。 - 撤销优先于确认:可逆改动立即写入(软删除)并通过
runUndoable提供 Undo;确认仅用于不可撤销操作,且需在契约中用whyNotUndo声明。 - 确认写明后果:标题含数量(如「Delete 3 done tasks?」)、按钮用动词、Cancel 在前;
confirm()返回'confirmed' | 'cancelled' | 'dismissed',仅'confirmed'执行。 - 无障碍公告:notice 和 banner 作为 polite live region(TalkBack);iOS 上无 live region,需用
useAnnounce()公告一次;iOS 上 notice 与确认通过AboveModalScreens覆盖窗口渲染,以显示在 modal 或表单页之上。 - 屏幕状态契约与测试:
contracts/screen-states.json列出每个屏幕的视图、测试、状态和标记;每个声明状态都要在测试中渲染。
使用方式
- 阅读
references/states-and-the-switch.md,运行node ${CLAUDE_SKILL_DIR}/scripts/check-ui-states.mjs .查看现状。 - 复制
templates/src/ui/feedback/(含测试)、templates/src/hooks/use-reachability.ts(含测试)以及templates/src/i18n/catalogs/的 catalog 键到每个语言。src/services/connectivity/来自 networking-and-server-state:仅当应用缺少时才复制模板,并让其 query manager 把 TanStack 的onlineManager接到该端口,否则离线状态不会出现。ui-component-library默认排除src/ui/feedback/,自定义排除目录时需保留feedback/**。 - 在根 Stack 外层挂载唯一的
FeedbackProvider(位于 MotionConfig 和 i18n provider 之下,参考examples/app-feedback.tsx),屏幕通过useFeedback()读取。 - 每个数据屏幕:在模型 hook 中构造事实并调用
listScreenStateOf(参考examples/use-tasks-model.ts),把文案集中到一个纯函数(templates/src/features/tasks/tasks-state-copy.ts),用一个 switch 渲染并配合延迟占位(templates/src/features/tasks/components/tasks-state-switch.tsx、examples/tasks-screen.tsx)。下拉刷新读取用户下拉标志(围绕refetch()使用useBusyAction),而非isRefreshing。 - 反馈:结果走
notify(),可逆改动走runUndoable,不可逆操作走confirm(),进行中动作走useBusyAction,离线横幅读取useReachability。 - 契约:复制
templates/contracts/screen-states.json及其 schema,列出每个加载/列表/筛选数据的屏幕和每个带whyNotUndo的确认。 - 测试:按模板测试渲染每个声明状态、notice 与确认结果;运行
npx jest src/ui/feedback src/features --ci和npm run typecheck。 - 再次运行检查脚本,修复每条 FAIL(规则 id 见
references/checker-rules.md)直到输出RESULT: PASS。
适用场景
适用于 Expo / React Native 应用中会加载、列出或筛选服务端数据的数据屏幕,以及需要统一提示、离线横幅、确认与撤销交互的界面。检查器还有两种边界输出:尚无数据屏幕时提示 contracts/screen-states.json: none yet (...) 并通过;package.json 无 react-native 时输出 NOT APPLICABLE: react-native is not in package.json。
限制与注意
- 本技能不替代数据层与组件库:ConnectivityPort、Toast/DialogCard/EmptyState 等分别由 networking-and-server-state、ui-component-library 等关联技能提供。
- 同一时刻只保留一个 notice,不排队;旧消息不会延迟补发。
- 反模式列表明示禁止:loading/error 双标志、
query.data ?? []判断空态、isFetching时返回 spinner、立即显示的居中 ActivityIndicator、默认 shimmer 循环占位、用 dialog 报结果、Alert.alert、离线用 toast、notice 排队、每次删除都「Are you sure?」、仅在'cancelled'时返回、只隐藏行的 Undo、仅靠 live region、refreshing={state.isRefreshing}、notice host 作为导航器普通兄弟节点或使用根 Modal。 - 检查脚本输出不含 SKIP 行;iOS 上的 notice/确认显示与部分无障碍行为需在真机验证。
- 具体收费、平台支持范围与效果数据,原文未提供。