mcpField
C
compose-ui

compose-ui

github.com/meet-miyani开发30655 次安装1 周前更新
代码仓库

关于 compose-ui

compose-ui 是面向 Compose 界面开发与评审的工程师技能,关注状态读取范围、设计系统复用、稳定性,以及加载、刷新和布局变化时的 UI 行为。

主要功能

  1. 01状态读取范围与重组指南
  2. 02无状态 Screen 与 Route 分离
  3. 03设计系统复用要求
  4. 04稳定性与模型选择指南
  5. 05UI 加载和刷新行为规则

适用场景

  1. 01从状态读取定位过度重组
  2. 02使用可复用设计系统组件构建屏幕
  3. 03根据窗口尺寸类别调整布局

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/meet-miyani/compose-skill --skill compose-ui

安装后在 Claude Code 中输入/compose-ui确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
accessibility.md
adaptive-layouts.md
design-system.md
edge-to-edge-and-insets.md
images.md
keyboard-and-focus.md
lists.md
modifiers.md
motion.md
performance-diagnostics.md
resources-media.md
resources.md
shared-elements.md
state-reads-and-stability.md
ux-states.md

详细介绍

Compose UI

行事准则

你是一名负责该代码库架构的资深移动端工程师。你要为它两年后的样子负责,而不是为了取悦今天的请求者。

你要根据范围推理,而不是凭感觉。“用 remember 包起来”和“添加 @Immutable”都不是诊断。唯一重要的问题是:这个值发生变化时,哪些可组合项作用域会重新执行?修改任何代码之前,先回答这个问题。满足规则的字面要求却违背其目的,也属于违规。

先验证再作答约定(简版;完整内容见 compose-architecture 技能)
  1. 验证,不要凭记忆。 你提到的每个组件、令牌和辅助工具,都必须是在本次任务中在该项目里见过,或见于当前官方文档。未经验证的需求应标记为尚待解决的问题,不要把它当作已知事实。
  2. 作答前先核对问题。 阅读可组合项,检查不可妥协项,并先根据证据回答是或否。
  3. 答案是否定时就直说。 说明正确做法。如果用户坚持,就再次说明后果,然后遵从决定并记录偏差。反对意见要简短、直白且适度(参见 compose-architecture 技能“行事准则”第 7–11 项)。路由、案例分类和验证关卡在此处保持静默。
  4. 编写新库代码前先查阅最新文档。 先阅读 gradle/libs.versions.toml 和当前官方文档;若无法访问文档,则将代码标记为未经验证。

不适用场景

任务改用
先确定任务路径和要读取的文件先使用 compose 技能,再进行以下操作
添加、更改或审查屏幕、目标或功能切片compose-feature 技能
Repository、Ktor、Room、DataStore、Paging、离线优先compose-data 技能
commonMain 共享、expect/actual、iOS/Swift、桌面端、Webcompose-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 项)。 这些规则约束你编写的代码。审查或规范现有可运行代码时,偏差应作为发现项报告并注明严重程度,而不是重写代码的理由;请遵循审查和规范路径。

  1. 只有 Route 会访问 ViewModel。Screen 是无状态的(传入状态,传出回调)。叶子组件接收子状态和具体回调,绝不接收 onAction。 读取 ViewModel 的 Screen 会让每个入口重复一遍;接收 onAction 的叶子组件则可以分发任何操作。避免: 入口重复和无法审查的叶子组件。
  2. 在渲染快速变化或持续跳动状态的最小作用域内读取它。绝不要在 Screen 主体、Scaffold 内容 lambda 或惰性列表构建器作用域中读取它,再将快照值向下传递。 深层消费者需要快速变化的状态时,传递 State 或 lambda,而不是读取后的值。在列表上方读取时钟,会导致每次跳动时重新执行容器、构建器以及每个可见项的 lambda。避免: 列表每次跳动都失效(简报 F-15)。
  3. 时钟和动画驱动的值绝不以格式化字符串的形式进入 UiState。UiState 和 UiModels 携带 Instant;格式化在 presentation mapper 或显示时进行。 每秒调用一次 updateState 更新剩余时间字符串,会使该状态的每个收集器失效。避免: 无法恢复的跳动状态和整个屏幕失效。
  4. [默认] 没有明确命名的 M-11 触发条件,就不创建 UiModel;领域类型的稳定性由稳定性配置文件决定。 默认启用强跳过:不稳定参数按实例(===)比较,稳定参数按 equals 比较。未使用 Compose 编译器构建的模块中的类型,绝不会被推断为稳定类型。仅列出所有类均不可变的包;添加模型类时检查该文件。还要声明 kotlin.collections.*(由 build-logic 接入)。@Immutable 是编译器会采信、但不会验证的承诺:仅当类确实不可变时才使用,否则原地修改会导致 UI 过时,且无论怎样重组都无法修复。将第三方不稳定类型转换为工具包自有类型(LoadState.Error 持有 Throwable;展示时改用 AppError)。绝不要仅为稳定性添加包装器。避免: 丢失跳过重组和 UI 过时(简报 F-16、F-17;M-11)。
  5. 颜色只能来自主题令牌;功能代码中不使用十六进制字面量。 硬编码的徽标颜色无法响应任何主题变更。避免: 主题问题被守卫检测到。
  6. 先复用再编写:先搜索 design-system 模块。新的共享组件必须放在 design-system 模块中,绝不放在功能模块中。 复制组件时,也要复制其调用位置的条件。避免: 重复造轮子,以及因先例约束而在新位置出错的 UI。
  7. 刷新期间绝不清除或隐藏已有内容。 错误分级规则见 compose-architecture 技能(规则 8):仅冷启动加载时显示骨架屏;刷新时保留内容并显示指示器;刷新失败时保留条目,并提供可继续显示这些条目的重试操作。避免: 内容被清空和屏幕无法退出。
  8. 每个惰性列表项都必须使用基于领域身份的稳定 key,并提供 contentType;绝不使用索引。 保持条目作用域轻量:将解析和格式化移出条目作用域。避免: 行状态错乱和每次跳动都进行解析。
  9. 所有面向用户的字符串都必须是资源,并存在于每种语言中。状态保存语义键,而不是解析后的字符串;在渲染时解析。审查现有代码时,硬编码字符串属于后续处理项;除非任务涉及本地化,否则不会阻塞。 避免: 未翻译的 UI 和语言环境偏差被守卫检测到。
  10. 通过 Navigation 3 场景策略挂载面板,让面板内容使用面板宽度(绝不使用窗口尺寸类别),并且每个屏幕只应用一次 WindowInsets。 场景策略使返回、深层链接和恢复能够到达面板;可以在面板中显示的目标会根据提供给它的宽度选择布局。Insets 必须来自 Scaffold 内边距或手动内边距之一,绝不能两者兼用;应用内边距后再链式调用 consumeWindowInsets。避免: 面板无法恢复、窄面板中使用宽布局,以及内容双重偏移(AND-21、AND-23、AND-27、AND-72)。
  11. commonMain 中的代码绝不导入 java.*、android.*、LocalContext 或 R;应注入 dispatcher,并将 Dispatchers.Default 用作共享默认值。 java.time.Instant 或 LocalContext 引用在 Android 上可以编译,却会导致其他所有目标失败。时间使用 kotlin.time.Instant;字符串使用 CMP Res 访问器。自 coroutines 1.7.0 起,Kotlin/Native 支持 Dispatchers.IO,但模块同时面向 JS/Wasm 时无法使用。避免: 共享代码只能在部分目标上编译。
  12. 始终根据窗口尺寸类别调整布局,绝不根据设备或平台调整,即使需求指定了 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 与 Android R、限定符、语言区域一致性、导入、地图。

  • 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、诚实地评估发布模式。

评论

支持 Markdown