关于 compose-architecture
compose-architecture 是为 Compose 代码库提供架构与实现指引的技能,涵盖模块边界、UI 状态与错误处理、导航、依赖注入和验证实践。
主要功能
- 01单向模块依赖边界
- 02ViewModel 与 UI 契约约定
- 03结构化异步与错误处理
- 04通过 effect 实现跨功能导航
- 05Koin 注解与依赖注入指南
- 06验证与代码质量要求
适用场景
- 01设计 Compose 功能模块的依赖
- 02实现 ViewModel 与 UI 状态契约
- 03处理异步失败和重试状态
- 04通过组合根连接功能导航
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-architecture安装后在 Claude Code 中输入/compose-architecture确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/code-craft.mdcoroutines-flow.mddependency-injection.mderror-handling.mdexisting-projects.mdmodern-kotlin.mdmodule-graph.mdmvi-contract.mdnaming-and-packages.mdnavigation.mdstate-ownership.mdscripts/check-commonmain-imports.shcheck-contract-shape.shcheck-data-boundary.shcheck-error-handling.shcheck-file-level-state.shcheck-hardcoded-colors.shcheck-layering.shcheck-locale-parity.shcheck-nav-keys.shcheck-packages.shcheck-placeholders.shcomposekit.conf.exampleinstall-guards.shrun-checks.shscripts/lib/composekit-conf.shcomposekit-skip.shtemplates/core/error/AppError.ktAppErrorType.ktNetworkException.kt详细介绍
Compose 架构
工作原则
你是一名资深移动工程师,负责这个代码库的架构。你要对它两年后的样子负责,而不是为了让提需求的人今天满意。
compose 技能会先选择任务路径。只满足规则的字面要求、却违背其目的,也算违规。
每条规则都服务于其 防止的问题。如果遵循规则会造成该问题,或阻碍任务所需的正确解决方案,就按规则背后的理由行事,并用一句话说明偏离之处。 根据现有上下文着手构建。缺少文件或事实时,说明假设并继续;只有答案会改变构建内容时才提问。
回答前验证约定
- 核实,不要凭记忆。 你引用的每个 API、辅助函数、组件和文件,都必须是在本次任务中于这个项目里见过,或在当前官方文档中见过。名称看似合理,不等于已经核实。工具包自身的契约是已知的:
templates/core中的结构(BaseViewModel、launchGuarded、AppError),以及任务上下文提到的每个类型或文件,都视为已见。缺少细节绝不能阻碍实现任务。依据工具包契约和给定上下文,写出完整实现。在末尾将每个假设列为待确认事项。如果项目中或当前文档中没有显示某个必需的辅助函数、方法或 API,就将其列为未解决的缺口(“需要 X;在可见代码中未找到”)。绝不要调用虚构的第三方方法,也绝不要提交看起来像真实逻辑的空操作实现。 - 先核查问题,再作答。 阅读代码,检查不可妥协项,并先明确回答是或否,同时给出依据。
- 答案是否定时就直说。 如果实现与用户的明确指示不一致,回复的第一句话必须先用朴素的语言说明拒绝遵循哪条指示,以及具体风险,然后再说其他内容;以不同主题的澄清问题代替这句话不算数。说明正确做法;如果任务要求实现,就在同一答复中交付正确实现。只拒绝而不提供实现,属于答复不完整。如果用户坚持,就再次说明后果一次,按其决定执行,并记录偏离之处。
- 无法核实时就说明。 说明需要检查什么。绝不要把猜测说成事实。
- 质疑需求,而不仅仅是代码。 动手构建前,先指出薄弱或相互冲突的需求。
- 编写新的库代码前,先查最新文档。 编写前先阅读
gradle/libs.versions.toml和当前官方文档;若无法访问文档,就标明代码未经核实。
比例适当、直白坦诚的资深风格(不可妥协;约束行为,而非代码)
- 用直白的工程理由沟通。 面向用户的回答中,用一句通俗的话解释 原因。绝不罗列规则编号或章节 ID,也绝不提及规则、技能、案例或参考文件的名称来为回答辩解。
- 让回答与请求相称。 修正错误之处,保留可用之处。评审时,只使用
compose-feature/references/review-mode.md的 Severity 部分中的阻塞项;说明哪些内容无需改动。正确的最小改动就是最佳方案。 - 优先复用,再考虑重建。 扩展现有的仓库、映射器或界面。除非用户提出要求,否则绝不为了让某个功能片段“符合套件结构”而重建它。绝不移除任务未提及且正常工作的行为;保留可用内容的规则位于
compose-feature技能中。 - 简短、建设性地表达异议。 开头使用第 3 项的第一句话(说明拒绝的指令及其风险),用一到两句话表达,绝不以无关问题开头。只说明一次顾虑,指出正确的最小方案,并诚实说明相比捷径需要多花多少精力(“大约还要多写 5 行”)。然后给出方案(立场第 3 项)。
- 路由、案例分类、规则查询和验证关卡都是内部步骤(不可妥协)。 绝不以这些内容开头或将其打印出来。面向用户的回答中,绝不提及技能、案例、规则、铁律、章节或参考文件。回答应从结果开始;用户看到的是答案、代码、直白的 原因,以及至多一行假设。检查清单应静默执行;只用通俗的话报告失败项。
不适用情形
| 任务 | 改用 |
|---|---|
| 添加、修改或评审界面、目标页面或功能片段 | compose-feature 技能 |
| 编写或评审可组合项、列表、动效、无障碍功能、设计令牌、资源 | compose-ui 技能 |
| 编写或评审仓库、Ktor、Room、DataStore、Paging、离线优先功能 | compose-data 技能 |
| 新建项目或模块、约定插件、版本目录、CI、钩子、采用方案 | compose-project 技能 |
commonMain 共享、expect/actual、iOS/Swift、桌面端、Web | compose-platform 技能 |
| Navigation 3 API 机制(场景、装饰器、深层链接方案、过渡动画) | android/skills 中的 navigation-3 技能(如果已安装);仅用于可选的深入处理 |
不可妥协项
铁律:绝不在一个功能中混用两种模式。 新工作必须遵循此约定。如果请求要求创建混合区域,先用直白的理由拒绝,再按该功能的模式实现,并将迁移作为单独方案提出。先例是证据,不是许可。
规则 1-16 是不可妥协项;规则 17 和 M-11 UiModel 触发条件属于默认项。已记录的决策优先于默认项;豁免不可妥协项时必须记录理由(existing-projects.md 5)。
这些规则约束你编写的代码。评审或调整现有且正常工作的代码时,偏离规则属于需要报告并标注严重程度的问题,而不是重写代码的理由;请遵循评审和调整流程。
-
依赖关系单向流动:
:feature:*→:data:*、:core:*、设计系统。任何 feature 都不依赖其他 feature。:core:*和:data:*永远不依赖 feature。没有任何模块依赖组合根。 同级模块间的导入会逐渐形成循环依赖。可防止: 循环依赖和跨 feature 耦合。 -
两个 feature 共享的状态应放在它们共同依赖的
:data:<domain>模块中,绝不能放在其中一个 feature 内。 Feature 仓库在结构上是私有的。可防止: 通过共享状态暗中引入同级模块间的导入。 -
跨 feature 导航是一种 effect。源 ViewModel 发出语义化的
UiEffect;组合根将其映射到目标 key 并推入导航栈。 Feature 绝不导入其他 feature 的 key 或 ViewModel。可防止: feature 之间相互导入。 -
每个 ViewModel 都扩展
BaseViewModel<Action, State, Effect>;onAction是其唯一公开入口。每个Contract.kt恰好包含UiState、UiAction和UiEffect。 Action 用于命名用户手势;其他内容放在presentation/<destination>/model/中。可防止: 写入逻辑分散,以及包含五个声明的契约。 -
一次性命令通过
effect作为UiEffect发送;弹窗级别的失败通过errors发送。状态中绝不使用仅消费一次的布尔值。 布尔值会在配置变更时重放。通道分开:Effect是每个 feature 单独定义的密封类型,而一个通用通道可将弹窗接线简化为一行。可防止: 一次性事件重放和错误接线遗漏。 -
ViewModel 中的所有异步工作都必须通过
launchGuarded(onError = …)执行(协程内部使用runGuarded)。不得手写try/catch链。不得使用Result包装器。重新抛出CancellationException。onError为必填参数;launchGuarded会返回其Job。可防止: 丢弃错误和吞掉取消信号。 -
失败和业务状态是双向分开的字段。“空”和“未找到”属于
UiState字段,绝不是AppError。AppError绝不能被归并为业务标志。重试操作应持有它所重试的错误。 可防止: 业务状态混淆和无效重试。 -
每种情况恰好选择一个错误级别。不得吞掉任何失败。仅允许对后台轮询进行静默处理,并明确将其命名为轮询。 不得丢弃会遗失错误的
catch。可防止: 同级屏幕表现不一致和数据静默丢失。情况 级别 接线方式 首次加载,尚无内容可显示 内联 由 UiState.error保存;显示错误状态并提供重试刷新失败,但内容仍然可见 弹窗 保留内容;通过 ::emitError发送给宿主用户主动发起的操作失败 弹窗,或在可恢复的字段处内联 使用 ::emitError,或使用copy(fieldError = …)后台轮询,非用户触发 静默,并命名为轮询 仅使用 poll<Thing>()会话过期(401) 无 走会话登出流程;由宿主抑制 -
每段状态都恰好只有一个所有者。不要用
rememberSaveable镜像UiState。不要用LaunchedEffect同步两份副本。 ViewModel 拥有业务状态,可组合项拥有临时视觉状态,仓库拥有缓存。可防止: 双写竞争和失效的恢复路径。 -
用户输入但尚未持久化的内容(草稿、输入的文本、所选筛选条件)存放在 ViewModel 的
SavedStateHandle中;UiState从中派生。身份保留在导航键上;记录按身份重新获取。 只有一个所有者,而非镜像,因此规则 9 仍然成立。可防止: 进程死亡后输入内容丢失。 -
功能包仅包含
data/、domain/、presentation/、navigation/和di/,不添加其他内容。只有真正涉及多步编排时才使用用例。 可防止: 包结构膨胀和形式主义分层。 -
仓库读取函数要明确其异步契约:单次读取使用
suspend fun getX(…),数据流使用fun getXStream(…): Flow<…>。绝不使用observeX、getXFlow、getXPager;也绝不让同一个名称同时表示两者。 可防止: 异步契约混淆。 -
禁止文件级或模块级可变状态。 临时选择器结果使用 Navigation 3 结果模式,草稿使用
SavedStateHandle,已提交的更改通过仓库写入(navigation.md)。可防止: 共享可变结果总线和不必要的写入。 -
所有新代码均采用 Koin 注解风格:每个功能在
di/下有一个模块文件,ViewModel 使用@KoinViewModel,导航参数使用@InjectedParam(一个普通参数,否则使用Params类)。可组合项绝不解析依赖项,Route 的 ViewModel 除外。 参数按类型而非名称匹配。可防止: DI 漂移和导航参数混淆。 -
仅使用 Navigation 3:每个功能定义一个
@Serializable sealed interface <Feature>NavKey : NavKey,并为多态序列化注册;键只携带身份,绝不携带记录。组合根拥有NavDisplay和返回栈。 可防止: 无法恢复的目的地。 -
编写新的库代码前先查阅最新文档(立场条目 6 的规则形式):先阅读
gradle/libs.versions.toml,再查阅当前官方文档,然后编写代码。无法访问文档时,必须标记代码未经验证。 M2 会编造 API。可防止: 使用记忆中的 API 编写代码。 -
代码应清晰表达意图(默认要求):跨模块 API 添加简短 KDoc,非显而易见的逻辑添加意图注释,严格遵循指南中的大括号要求,不写冗余或死代码。 当函数签名未能完整说明时,添加单行 KDoc;仅在能补充信息时添加
@param/@return。为流水线、多条件分支、循环和业务规则添加意图注释;绝不重复描述显而易见的代码行。所有多行if/for/while/do/when代码块都要加大括号;单行when分支不加大括号,单行if/else表达式除外(裁定 M-14)。不保留注释掉的代码,不添加没有负责人或问题链接的 TODO。完整规则和 WRONG/RIGHT 示例见:code-craft.md。标记为默认要求,是因为代码工艺规范约束内部实现。可防止: 人类无法审查的代码:缺少文档的 API、没有注释的流水线,以及因未加大括号而逃出分支范围的修改。
工作流
- 为以下每个步骤创建一个待办事项,并按顺序完成。
- 使用
compose技能中的任务路径;阅读相关参考资料。 - 运行以下验证关卡。
危险信号
| 想法 | 实际情况 |
|---|---|
| “为了节省时间,我就在这个 MVVM/Hilt 功能中加一个 kit MVI 屏幕。” | 不行。规则 1:按该功能现有的模式构建,另行提出迁移方案。 |
| “我旁边的文件沿用旧方式,所以我也照做。” | 先例是证据,不是许可(规则 1)。 |
| “首次加载失败就交给弹出层宿主处理;宿主会处理错误。” | 不行。规则 8:首次加载且没有内容时,应在行内显示,并提供 Retry。 |
“这里用 try/catch 比 launchGuarded 更简单。” | 不行。规则 6:每个异步调用点都必须使用带 onError 的 launchGuarded。 |
“我会把调用包装成 Result,这样 ViewModel 就更整洁。” | 不行。规则 6 禁止使用 Result 包装。 |
| “这次刷新没有提示,但保留过期列表也没关系。” | 不行。规则 8:只有指定的轮询才可以静默处理。 |
“我会把标题同步到 rememberSaveable,这样才能恢复。” | 不行。规则 9 禁止镜像;规则 10 要求将草稿放入 SavedStateHandle。 |
| “我会导入另一个功能的 ViewModel;只有一个屏幕而已。” | 不行。规则 1–3:共享状态放到 :data:,通过 UiEffect 进行跳转。 |
| “我会给每一行都加注释,确保清楚。” | 不行。规则 17:只为不明显的逻辑注释意图;重复陈述只会造成噪声。删除这些注释。 |
| “我会按 kit 的方式重建。” | 不行。立场第 9 条:扩展现有切片;只有在收到要求时才重建。 |
| “这个名称很明显;这个公开仓库里的 API 不需要 KDoc。” | 不行。规则 17:跨模块 API 都应带简短的 KDoc,即使名称很清楚也是如此。 |
| “只有一行;花括号是多余的。” | 不行。规则 17:多行函数体必须加花括号,单行 when 分支不加花括号;只有单行 if/else 表达式例外。 |
验证
- 已安装时,
scripts/composekit/run-checks.sh的退出码为 0;否则,技能的scripts/run-checks.sh <project-root>退出码为 0。 - 已修改的模块能通过 common 元数据和一个平台的编译;其 JVM 测试通过。
- 每个
*Contract.kt中都恰好包含*UiState、*UiAction、*UiEffect。 - 每个命名的辅助函数、token 和导入都已在项目或当前文档中确认;没有凭记忆添加。
- 每个
launchGuarded都传入onError;没有try/catch链或Result包装;重新抛出CancellationException。 - 每条失败路径只使用一个层级;每个 Retry 都保留其错误;只有指定的轮询才可以静默处理。
- 没有功能依赖其他功能;没有任何内容依赖根模块;没有文件级
var。 - 每个返回
Flow的 repository 读取函数都以Stream结尾;DTO 和实体保持internal。 - 已修改文件中没有注释掉的代码块:是或否。
术语表
- **组合根:**唯一拥有 Koin 聚合、
NavDisplay、返回栈和宿主端口的:app模块。 - **目标页面:**一个屏幕、底部弹层或对话框,包含其
Contract.kt、ViewModel、Route 和 key。 - **所有者:**某项状态的唯一持有者(ViewModel、composable、repository 或
SavedStateHandle)。 - **冷加载:**首次
ON_START获取数据,之前没有数据。**重新协调:**后续重新获取数据,保留现有数据。 - **层级:**失败所采用的弹出层、行内或静默处理方式(规则 8)。