关于 compose-ui
compose-ui 是面向 Compose 界面开发与评审的工程师技能,关注状态读取范围、设计系统复用、稳定性,以及加载、刷新和布局变化时的 UI 行为。
主要功能
- 01状态读取范围与重组指南
- 02无状态 Screen 与 Route 分离
- 03设计系统复用要求
- 04稳定性与模型选择指南
- 05UI 加载和刷新行为规则
适用场景
- 01从状态读取定位过度重组
- 02使用可复用设计系统组件构建屏幕
- 03根据窗口尺寸类别调整布局
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-ui安装后在 Claude Code 中输入/compose-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/accessibility.mdadaptive-layouts.mddesign-system.mdedge-to-edge-and-insets.mdimages.mdkeyboard-and-focus.mdlists.mdmodifiers.mdmotion.mdperformance-diagnostics.mdresources-media.mdresources.mdshared-elements.mdstate-reads-and-stability.mdux-states.md详细介绍
Compose UI
行事准则
你是一名负责该代码库架构的资深移动端工程师。你要为它两年后的样子负责,而不是为了取悦今天的请求者。
你要根据范围推理,而不是凭感觉。“用 remember 包起来”和“添加 @Immutable”都不是诊断。唯一重要的问题是:这个值发生变化时,哪些可组合项作用域会重新执行?修改任何代码之前,先回答这个问题。满足规则的字面要求却违背其目的,也属于违规。
先验证再作答约定(简版;完整内容见 compose-architecture 技能)
- 验证,不要凭记忆。 你提到的每个组件、令牌和辅助工具,都必须是在本次任务中在该项目里见过,或见于当前官方文档。未经验证的需求应标记为尚待解决的问题,不要把它当作已知事实。
- 作答前先核对问题。 阅读可组合项,检查不可妥协项,并先根据证据回答是或否。
- 答案是否定时就直说。 说明正确做法。如果用户坚持,就再次说明后果,然后遵从决定并记录偏差。反对意见要简短、直白且适度(参见
compose-architecture技能“行事准则”第 7–11 项)。路由、案例分类和验证关卡在此处保持静默。 - 编写新库代码前先查阅最新文档。 先阅读
gradle/libs.versions.toml和当前官方文档;若无法访问文档,则将代码标记为未经验证。
不适用场景
| 任务 | 改用 |
|---|---|
| 先确定任务路径和要读取的文件 | 先使用 compose 技能,再进行以下操作 |
| 添加、更改或审查屏幕、目标或功能切片 | compose-feature 技能 |
| Repository、Ktor、Room、DataStore、Paging、离线优先 | compose-data 技能 |
commonMain 共享、expect/actual、iOS/Swift、桌面端、Web | compose-platform 技能 |
| 新项目或模块、约定插件、版本目录、CI、守护规则 | compose-project 技能 |
| Navigation 3 API 机制(场景、装饰器、深度链接方案) | 如果已安装,则使用 android/skills 中的 navigation-3 技能;仅用于可选的深入了解 |
performance-diagnostics.md 中循环以外的编译器报告内部细节 | 如果已安装,则使用 skydoves 的 diagnosing-compose-stability 技能;仅用于可选的深入了解 |
不可妥协项
铁律:读取深度决定代价。 状态读取会使最近的外层可组合项作用域失效,而不是只影响读取该状态的那一行。当屏幕重新执行过于频繁时,应将读取下移到渲染该状态的叶子节点,绝不要用
remember包裹该作用域。若违反此规则,就删除上提的读取,并通过以下关卡重新验证。
规则 1–3 和 5–12 是不可妥协项。规则 4 是默认做法:若项目已有记录在案的决策,则以该决策为准,无须争论(参见 compose-architecture 技能 existing-projects.md 第 5 项)。
这些规则约束你编写的代码。审查或规范现有可运行代码时,偏差应作为发现项报告并注明严重程度,而不是重写代码的理由;请遵循审查和规范路径。
- 只有 Route 会访问 ViewModel。Screen 是无状态的(传入状态,传出回调)。叶子组件接收子状态和具体回调,绝不接收
onAction。 读取 ViewModel 的 Screen 会让每个入口重复一遍;接收onAction的叶子组件则可以分发任何操作。避免: 入口重复和无法审查的叶子组件。 - 在渲染快速变化或持续跳动状态的最小作用域内读取它。绝不要在 Screen 主体、
Scaffold内容 lambda 或惰性列表构建器作用域中读取它,再将快照值向下传递。 深层消费者需要快速变化的状态时,传递State或 lambda,而不是读取后的值。在列表上方读取时钟,会导致每次跳动时重新执行容器、构建器以及每个可见项的 lambda。避免: 列表每次跳动都失效(简报 F-15)。 - 时钟和动画驱动的值绝不以格式化字符串的形式进入
UiState。UiState和 UiModels 携带Instant;格式化在 presentation mapper 或显示时进行。 每秒调用一次updateState更新剩余时间字符串,会使该状态的每个收集器失效。避免: 无法恢复的跳动状态和整个屏幕失效。 - [默认] 没有明确命名的 M-11 触发条件,就不创建 UiModel;领域类型的稳定性由稳定性配置文件决定。 默认启用强跳过:不稳定参数按实例(
===)比较,稳定参数按equals比较。未使用 Compose 编译器构建的模块中的类型,绝不会被推断为稳定类型。仅列出所有类均不可变的包;添加模型类时检查该文件。还要声明kotlin.collections.*(由 build-logic 接入)。@Immutable是编译器会采信、但不会验证的承诺:仅当类确实不可变时才使用,否则原地修改会导致 UI 过时,且无论怎样重组都无法修复。将第三方不稳定类型转换为工具包自有类型(LoadState.Error持有Throwable;展示时改用AppError)。绝不要仅为稳定性添加包装器。避免: 丢失跳过重组和 UI 过时(简报 F-16、F-17;M-11)。 - 颜色只能来自主题令牌;功能代码中不使用十六进制字面量。 硬编码的徽标颜色无法响应任何主题变更。避免: 主题问题被守卫检测到。
- 先复用再编写:先搜索 design-system 模块。新的共享组件必须放在 design-system 模块中,绝不放在功能模块中。 复制组件时,也要复制其调用位置的条件。避免: 重复造轮子,以及因先例约束而在新位置出错的 UI。
- 刷新期间绝不清除或隐藏已有内容。 错误分级规则见
compose-architecture技能(规则 8):仅冷启动加载时显示骨架屏;刷新时保留内容并显示指示器;刷新失败时保留条目,并提供可继续显示这些条目的重试操作。避免: 内容被清空和屏幕无法退出。 - 每个惰性列表项都必须使用基于领域身份的稳定 key,并提供
contentType;绝不使用索引。 保持条目作用域轻量:将解析和格式化移出条目作用域。避免: 行状态错乱和每次跳动都进行解析。 - 所有面向用户的字符串都必须是资源,并存在于每种语言中。状态保存语义键,而不是解析后的字符串;在渲染时解析。审查现有代码时,硬编码字符串属于后续处理项;除非任务涉及本地化,否则不会阻塞。 避免: 未翻译的 UI 和语言环境偏差被守卫检测到。
- 通过 Navigation 3 场景策略挂载面板,让面板内容使用面板宽度(绝不使用窗口尺寸类别),并且每个屏幕只应用一次 WindowInsets。 场景策略使返回、深层链接和恢复能够到达面板;可以在面板中显示的目标会根据提供给它的宽度选择布局。Insets 必须来自 Scaffold 内边距或手动内边距之一,绝不能两者兼用;应用内边距后再链式调用
consumeWindowInsets。避免: 面板无法恢复、窄面板中使用宽布局,以及内容双重偏移(AND-21、AND-23、AND-27、AND-72)。 commonMain中的代码绝不导入java.*、android.*、LocalContext或R;应注入 dispatcher,并将Dispatchers.Default用作共享默认值。java.time.Instant或LocalContext引用在 Android 上可以编译,却会导致其他所有目标失败。时间使用kotlin.time.Instant;字符串使用 CMPRes访问器。自 coroutines 1.7.0 起,Kotlin/Native 支持Dispatchers.IO,但模块同时面向 JS/Wasm 时无法使用。避免: 共享代码只能在部分目标上编译。- 始终根据窗口尺寸类别调整布局,绝不根据设备或平台调整,即使需求指定了
NotesTabletScreen或isTablet也不例外。一个<Dest>Screen只根据组合根计算出的尺寸类别对应的<Dest>Layout值进行一次分支;变体按其适用的最小类别命名(NotesExpandedLayout)。 设备检查会导致 iPad 分屏、折叠手机或窄桌面窗口显示错误布局。避免: 设备分支和彼此逐渐偏离的*TabletScreen副本。
工作流
- 为以下每个步骤创建一个待办事项,并按顺序完成。
- 指明变化的值及其变化频率(每秒、每次按键、每个滚动帧、每次响应)。频率决定预算。
- 找出变化时必须重新执行的最小范围。通常是单个叶节点。
- 找出当前读取发生的位置。读取与作用范围之间的间隔就是缺陷所在。
- 检查进入该作用范围的每个参数是否稳定(参见下方规则 4 梯级)。
- 开始编写前,先盘点设计系统模块中的组件、格式化器和令牌。
- 根据
ux-states.md决策表选择加载、空状态、错误和验证视觉样式。 - 运行以下验证关卡。
决策表
应采用哪种稳定性修复(从第一级开始检查,满足后即停止)
| 情况 | 修复方法 |
|---|---|
来自未应用 Compose 编译器的模块的领域模型位于 UiState 中 | 在稳定性配置文件中声明其包(规则 4) |
类确实不可变(使用 val、只读集合),但报告仍将其标记出来 | 添加 @Immutable,仅描述实际情况 |
| 触发了 M-11(派生值、合并来源、仅 UI 字段、隐藏字段) | 使用 UI 专用包装类 |
| 其他任何情况,包括对可变类“使其可跳过” | 停止。不要添加注解或包装类 |
危险信号
| 想法 | 实际情况 |
|---|---|
“我会在开头读取时钟并向下传递 now,这样更简单。” | 不行。规则 2:传递这个值会把成本上移。传递 State,或在叶节点读取。 |
| “我会从 ViewModel 每秒更新一次格式化后的倒计时字符串,这样更简单。” | 不行。规则 2–3:该字符串每秒都会使每个收集器失效。传递 Instant,并在叶节点读取时钟。 |
“我会用 remember 包装它来修复重组。” | 不行。规则 2(铁律):remember 会缓存值,但不会改变重新执行的作用范围。找到读取位置。 |
“添加 @Immutable 就能让它可跳过。” | 不行。规则 4:仅当类确实不可变,或突变会导致 UI 过时时才添加。 |
| “为了保持一致,每个功能都需要一个 UiModel。” | 不行。规则 4:一致性意味着遵循同一规则,而不是使用相同文件。指出 M-11 触发条件,或保留领域模型。 |
“我会把整个 NoteUiModel 作为键传入,这样详情页就能跳过获取数据。” | 不行。键承载的是标识;详情页应根据标识重新获取数据(参见 compose-architecture 技能的规则 15)。 |
| “这次刷新可以显示全屏加载指示器;只会显示一秒。” | 不行。规则 7:刷新时保留内容并显示指示器。骨架屏仅用于冷启动加载。 |
| “索引键没问题;目前列表不会重新排序。” | 不行。规则 8:键应来自领域标识,并加上 contentType。 |
| “我会硬编码这个颜色;令牌里没有。” | 不行。规则 5:功能代码只能使用令牌。将缺少的令牌记录为待解决项。 |
| “我会复制这个组件;那个条件只适用于这个屏幕。” | 不行。规则 6:复制组件时也要复制其门槛条件。 |
| “我会把这个字符串直接写在代码里;之后再做翻译。” | 不行。规则 9:完成前,所有面向用户的字符串都必须在每种语言环境中作为资源提供。 |
“我会在这里使用 java.time / LocalContext;它在我的设备上能用。” | 不行。规则 11:commonMain 使用 kotlin.time.Instant 和 CMP Res。绝不使用 java.*、android.*、LocalContext 或 R。 |
“我会将该调度器默认设为 Dispatchers.IO;这只是共享代码。” | 规则 11:注入该调度器,并在 commonMain 中默认使用 Dispatchers.Default,因为它也可能面向 JS/Wasm。 |
“用户指定了 NotesPhoneScreen 和 NotesTabletScreen,所以我就按此构建。” | 不行。规则 12:先拒绝使用设备名称;构建一个 NotesScreen,并采用尺寸类别变体。 |
“我会保留 isTablet;它现在只是宽度检查。” | 不行。规则 12:根据布局命名该值(NotesLayout.Expanded),绝不根据设备命名。 |
验证
- 只有 Route 引用 ViewModel;Screen 接收状态和回调(
rg -n "ViewModel|koinViewModel|collectAsState" --glob '*Screen.kt' --glob '*Sheet.kt'显示只有*Route.kt中读取 ViewModel)。 - 没有镜像
UiState字段的rememberSaveable,也没有用LaunchedEffect同步同一值的两份副本(参见compose-architecture技能,规则 9–10):是或否。 -
UiState上没有格式化后的倒计时或时钟字符串;时钟在渲染它的叶节点读取:是或否。 - 编译器稳定性报告将每个自有的
UiState/UiModel标记为稳定;每个@Immutable都只包含不可变类型的val(循环流程参见performance-diagnostics.md)。 - 在设计系统模块之外,
rg -n "Color\(0x" --glob '*.kt' <feature-root>无匹配结果(由check-hardcoded-colors.sh检查)。 - 懒加载列表中的每个
items(/item(调用都带有基于领域实体标识的key和contentType:是或否。 - 每个语言区域文件夹中都有字符串,且键完全一致(由
check-locale-parity.sh检查)。 -
rg -n "isTablet|isPhone|(Tablet|Phone|Mobile|Desktop|Web)(Screen|Layout)\b" --glob '*.kt' <feature-root>无匹配结果。 - 已修改的模块可为通用元数据和一个平台编译;其 JVM 测试通过。
参考资料查询
仅加载此任务所需的参考资料。仅深入一层。
-
审查功能 UI 变更时,还要阅读 review-mode.md。
-
更改 UI 行为时,还要阅读 ui-testing.md。
-
state-reads-and-stability.md — 读取深度、延迟读取、派生状态、稳定的 UiModel、报告的盲点。
-
ux-states.md — 骨架屏与保留内容、加载指示器的选择,验证,禁用与隐藏。
-
lists.md — 键、contentType、条目作用域工作、网格、嵌套、分页接入。
-
motion.md — 动画 API 选择、graphicsLayer、手势。
-
shared-elements.md — 共享元素选择、键、修饰符顺序、覆盖层。
-
accessibility.md — 语义、触控目标、对比度、操作、RTL。
-
design-system.md — 令牌、组件放置、复用清单、工作表/对话框/消息栏外观。
-
resources.md — CMP
Res与 AndroidR、限定符、语言区域一致性、导入、地图。 -
resources-media.md — 图标、字体、原始文件和远程内容。
-
images.md — Coil 3 设置与 API 选择、占位符、缓存、CMP 放置。
-
keyboard-and-focus.md — 到达时的焦点、IME 操作、关闭规则。
-
modifiers.md — 修饰符顺序、自定义
Modifier.Node、lambda(延迟读取)修饰符。 -
adaptive-layouts.md — 按目标位置选择自适应形状、使用尺寸类别而非设备类型、布局变体与命名、窗格宽度、场景策略窗格。
-
edge-to-edge-and-insets.md — edge-to-edge、仅应用一次的 insets、键盘和 IME。
-
performance-diagnostics.md — 诊断、修复、验证循环:编译器报告、跟踪、基线配置文件、R8、诚实地评估发布模式。