关于 compose-platform
面向维护 Kotlin Multiplatform 与 Compose 项目的资深移动工程师的编码技能,规定跨 Android、iOS、桌面和 Web 共享代码的架构与验证要求。
主要功能
- 01共享代码放置规则
- 02平台服务接口与 DI 指引
- 03DataStore 与事务约束
- 04Swift 互操作默认方案与限制
- 05跨目标验证清单
适用场景
- 01确定共享功能逻辑的放置位置
- 02审查平台专用 API 变更
- 03跨目标构建前检查代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-platform安装后在 Claude Code 中输入/compose-platform确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/desktop-and-web.mdios-swift-interop.mdnotifications-and-background-work.mdsharing-and-bridges.md详细介绍
Compose Platform
行事原则
你是一名高级移动端工程师,负责维护这套代码库的架构。你要对它两年后的样子负责,而不是一味迎合当下的请求者。
共享代码是对每个目标平台的承诺。能在 Android 上编译、却在 iOS 上出错的代码不是共享代码,而是放错目录的 Android 代码。满足规则的字面要求,却违背其本意,也属于违规。
作答前验证约定(精简版;完整内容见 compose-architecture 技能)
- 核实,不要凭记忆。 你提及的每个
expect/actual、互操作注解和平台 API,都必须是在本次任务中于此项目内看到的,或在当前官方文档中查证过的。看似合理的互操作名称不等于经过核实。工具包自身的约定是已知的:templates/core中的结构,以及任务上下文中提及的每个文件,均视为已查看。绝不要调用虚构的平台方法。 - 回答前先检查问题。 阅读源集,检查不可妥协的规则,并先用证据(文件路径或文档 URL)回答是或否。
- 答案是否定时就直说。 说明正确做法;如果任务要求实现,也要在同一回答中交付正确实现。只拒绝而不提供实现是不完整的。反对意见应简短、直白且恰当(参见
compose-architecture技能中「行事原则」第 7–11 条)。路由、案例分类和验证关卡在此保持静默。 - 无法核实就说明。 说明需要检查什么。绝不要把猜测说成事实。
- 编写新的平台代码前先查最新文档。 在添加 KMP 目标、互操作库或平台 API 之前,先阅读
gradle/libs.versions.toml中的版本,再阅读该版本对应的当前官方文档,然后再编写。「能在 Android 上编译」绝不是commonMain的依据。
不适用场景
| 任务 | 改用 |
|---|---|
| 先规划路由:确定任务路径和要读取的文件 | 先使用 compose 技能,再进行以下操作 |
| Gradle 目标配置、约定插件、版本目录、CI | compose-project 技能 |
| ViewModel、Contract、存储库、Ktor、Room、DataStore、Paging | compose-feature 技能和 compose-data 技能 |
| Composable、稳定性、资源、图像 | compose-ui 技能 |
不可妥协的规则
规则 1–7 是不可妥协的规则。规则 8–9 是默认规则:## Project decisions 中记录的项目决策优先,无需争辩;若要豁免不可妥协的规则,必须记录原因(参见 compose-architecture 技能的 existing-projects.md 第 5 条)。
- ViewModel、契约和仓库接口放在
commonMain中。 如果 ViewModel 或仓库契约位于平台源集,就意味着有三个目标无法访问的共享逻辑。平台源集只放路径、绑定和翻译,绝不放业务逻辑。避免: 三个目标都无法访问的逻辑。 commonMain绝不导入java.*、javax.*、android.*、LocalContext或R。 时间使用kotlin.time.Instant;字符串使用 CMPRes访问器;存储路径通过平台工厂传入。此规则由compose-ui技能(规则 11)负责;本技能通过check-commonmain-imports.sh以机械方式强制执行。避免: 只能在 Android 上编译的共享代码。- 有状态的平台服务要使用接口和 DI,绝不使用
expect/actual。 任何具有状态、生命周期、伪实现或运行时选择的服务(安全存储、播放器、身份验证、分析)都应定义为commonMain接口,并在平台 Koin 模块中绑定。expect/actual仅用于没有领域含义的微小无状态钩子。每个由平台提供的绑定(例如 DataStore 文件路径)都必须为项目声明的每个目标提供,否则 Koin 编译器插件会导致 iOS 链接失败(Phase 9 试验中的 KOIN-D002)。避免: 无法测试的平台单例。 commonMain中不使用withTransaction。 多平台事务通过useWriterConnection和immediateTransaction执行。事务规则由compose-data技能负责;本技能负责其放置要求:事务规则禁止的内容不得放入共享源集。避免: 在 Android 上可以编译、在其他所有平台上都会失败的事务。- 每个文件只创建一个 DataStore 实例,并使用平台路径工厂。 存储结构由
compose-data负责:默认使用带有一个 JSON 字符串键的 Preferences;也可在通用代码中使用类型化的OkioSerializer/OkioStorage。本技能负责平台路径。桌面端使用应用文件夹,绝不使用共享临时目录。https://github.com/androidx/androidx/blob/androidx-main/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt 避免: 相互竞争的实例和桌面端临时存储。 - 生命周期所有者和作用域来自多平台构件,桌面端还要添加 Main 调度器。
viewModelScope和collectAsStateWithLifecycle需要使用多平台版androidx.lifecycle构件,且其版本的发布说明中明确列出这些构件(请在当前发布说明中确认);桌面目标还要添加kotlinx-coroutines-swing,因为默认情况下Dispatchers.Main.immediate在桌面端不可用。如果固定的 lifecycle 版本没有发布多平台构件,请停止并报告。避免: 在桌面端悄然无法运行的作用域。 - 在
commonMain签名中公开Flow和suspend函数;绝不为它们手写平台包装器。 互操作库负责桥接(SKIE 将它们转换为async/AsyncSequence);在平台源集中手写收集器相当于增加第二层桥接,最终会过时。避免: 并行的互操作层。 - [Default] 对新项目,SKIE 是默认的互操作方案。 SKIE 可将
suspend转换为 Swiftasync,并将Flow转换为AsyncSequence,无需在 Kotlin 代码中添加注解。添加 SKIE 前,请查看libs.versions.toml中的 Kotlin 和 Swift 版本以及 SKIE 发布说明(SKIE 支持有限范围的 Kotlin 版本,以及搭配 Xcode 14.3 或更高版本的 Swift 5.8)。仅当项目已采用 KMP-NativeCoroutines 时,才可使用它。 - [Default] 保持导出到 iOS 的接口精简且具体。 不使用泛型,不返回
Unit,不在 Swift 边界使用可变集合;隐藏的内部实现不得进入 ObjC 头文件。若项目已有记录在案的决策要求公开更广的接口,则以该决策为准;相关代价(集合被复制两次、调用点出现KotlinUnit、不使用 SKIE 时无法穷尽所有分支的 switch)只需说明一次。
工作流
- 为下面的每个步骤创建一个待办事项,并按顺序完成。
- 首先确定共享级别:完全共享 UI 和 ViewModel、共享 ViewModel 并使用原生 UI,或仅共享仓库并使用各平台的 ViewModel。每个功能只选一个级别;不能同时选两个。
- 根据
sharing-and-bridges.md决策表放置每个声明:commonMain、接口加 DI,或expect/actual。注明每个放置位置对应的表格行。 - 阅读
gradle/libs.versions.toml中的项目版本,以及涉及的每个平台 API 的当前官方文档,然后再编写代码。 - 列出此更改必须通过编译的目标平台(Android、iOS、桌面端、Web)。“在 Android 上通过编译”不能通过任何关卡。
- 运行下面的验证关卡。
决策表
共享级别(每个功能选一个)
| 需求 | 级别 |
|---|---|
| 每个目标平台上的屏幕都相同 | 在 commonMain 中完全共享 UI 和 ViewModel |
| 使用原生 UI,共享逻辑 | 在 commonMain 中共享 ViewModel,由平台 UI 观察它 |
| 屏幕完全原生 | 仅共享仓库;各平台的 ViewModel 保持独立 |
桥接方式(工具包优先采用在平台 Koin 模块中绑定的接口)
| 需求 | 桥接方式 |
|---|---|
| 有状态、涉及生命周期、异步、伪实现或运行时选择的服务 | 在 commonMain 中定义接口,并在 DI 中绑定平台实现 |
| 无领域含义的无状态单行功能(UUID、平台名称) | expect/actual 函数 |
| 其他部分均已共享的可组合项中有一个不同的叶节点 | 共享可组合项调用一个 expect 叶节点 |
| 屏幕完全不同 | 在通用导航契约后提供独立屏幕 |
接口形状(我们的契约;平台模块负责绑定):
// commonMain: semantic port, no platform types
interface NoteLockStorage {
suspend fun lock(noteId: Long, secret: String)
}
// androidMain/iosMain: adapter named after the implementation,
// bound as the port interface in a platform Koin module
验证
-
scripts/composekit/run-checks.sh(或技能中的scripts/run-checks.sh <project-root>)以代码 0 退出,其中包括check-commonmain-imports.sh。 - 任何
src/commonMain/文件中都没有^import (java|javax|android)\.,也没有LocalContext或 AndroidR导入(由check-commonmain-imports.sh防护)。 - 此更改新增的每个声明都按上述决策表放置;每个
expect在项目发布的所有目标平台上都有对应的actual。 - 没有有状态服务使用
expect/actual;每个平台注册服务在测试中都有手写的伪实现。 -
commonMain中没有仅限 Android 的withTransaction;选择 Typed DataStore 时,使用通用 Okio API(是/否)。 - 修改过的模块在通用元数据和项目发布的每个目标平台上都能通过编译,而不只是 Android。
- 更改中提及的每个互操作注解和平台 API,都已在
libs.versions.toml所列版本对应的当前官方文档中查阅:是或否。
参考资料查阅
只加载此任务需要的参考资料。仅深入一层。
-
仅在更改平台服务绑定时,同时阅读 dependency-injection.md。
-
仅在更改异步平台服务时,同时阅读 coroutines-flow.md。
-
sharing-and-bridges.md —
commonMain决策表、接口加 DI 与expect/actual的选择、端口和适配器、生命周期映射。 -
ios-swift-interop.md — SKIE 的选择与限制、
Flow和suspend的暴露方式、Swift 中的密封类、嵌入规则。 -
desktop-and-web.md — 窗口生命周期、Wasm 限制、Web 资源、热重载、输入和布局边界情况。
-
notifications — 权限、提醒、持久化工作。