关于 kotlin-multiplatform
kotlin-multiplatform 是面向 Amethyst 中 Kotlin Multiplatform 架构决策的指导技能。它帮助判断代码应放入共享源集还是保留平台专属,并提供代码库示例及当前和未来目标平台的建议。
主要功能
- 01判断共享代码与平台专属代码
- 02源集依赖关系指导
- 03expect/actual 使用模式
- 04JVM 与目标平台兼容性建议
- 05常见抽象设计问题
适用场景
- 01为新代码选择源集
- 02检查 commonMain 中的平台 API
- 03规划 Android、Desktop 和 iOS 间的代码共享
- 04评估面向未来 web 或 wasm 目标的抽象
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vitorpamplona/ amethyst --skill kotlin-multiplatform安装后在 Claude Code 中输入/kotlin-multiplatform确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/abstraction-examples.mdexpect-actual-catalog.mdsource-set-hierarchy.mdtarget-compatibility.mdscripts/suggest-kmp-dependency.shvalidate-kmp-structure.sh详细介绍
Kotlin Multiplatform:平台抽象决策
为 Amethyst 中的 KMP 架构提供专家指导:决定哪些内容应该共享,哪些应该保留为平台专属。
何时使用此技能
进行平台抽象决策时:
- “我应该创建 expect/actual,还是仅保留 Android 版本?”
- “我可以共享这段 ViewModel 逻辑吗?”
- “这段加密/JSON/网络实现应该放在哪里?”
- “这里用到了 Android Context——可以抽象出来吗?”
- “这段代码放错模块了吗?”
- 为 iOS/web/wasm 目标做准备
- 检查是否存在放置不当的代码
抽象决策树
核心问题:“这段代码是否应该在多个平台间复用?”
按以下决策路径进行判断(不到 1 分钟):
Q: Is it used by 2+ platforms?
├─ NO → Keep platform-specific
│ Example: Android-only permission handling
│
└─ YES → Continue ↓
Q: Is it pure Kotlin (no platform APIs)?
├─ YES → commonMain
│ Example: Nostr event parsing, business rules
│
└─ NO → Continue ↓
Q: Does it vary by platform or by JVM vs non-JVM?
├─ By platform (Android ≠ iOS ≠ Desktop)
│ → expect/actual
│ Example: Secp256k1Instance (uses different security APIs)
│
├─ By JVM (Android = Desktop ≠ iOS/web)
│ → jvmAndroid
│ Example: Jackson JSON parsing (JVM library)
│
└─ Complex/UI-related
→ Share it in commonsUI; expect/actual or a slot only for the platform leaf
Example: Navigation. One shell in commonsUI, picked by window size
(ScreenLayoutSpec); only the Activity / Window entry point is native
Final check:
Q: Maintenance cost of abstraction < duplication cost?
├─ YES → Proceed with abstraction
└─ NO → Duplicate (simpler)
代码库中的真实示例
加密 → expect/actual:
// commonMain - expect declaration
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}
// androidMain - uses Android Keystore
// jvmMain - uses Desktop JVM crypto
// iosMain - uses iOS Security framework
**原因:**各平台的安全 API 不同。
JSON 解析 → jvmAndroid:
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}
**原因:**Jackson 仅适用于 JVM,可在 Android 和 Desktop 上运行,但不能用于 iOS/web。
导航 → 共享;入口点 → 平台专属:
- 共享(迁移至此):导航主机、底部栏/侧边栏/永久抽屉,根据窗口大小通过
ScreenLayoutSpec选择 - Android:由
MainActivity承载;Desktop:由Window(加上 MenuBar、托盘)承载 **原因:**Android 也可以在笔记本电脑上运行,而新的 Desktop 应用会呈现相同的 UI。参见commons/plans/2026-09-27-one-ui-android-desktop.md。目前desktopApp的侧边栏外壳已属旧版。
心智模型:将源集视为依赖关系图
将源集视为依赖关系图,而非文件夹。
┌─────────────────────────────────────────────┐
│ commonMain = Contract (pure Kotlin) │
│ - Business logic, protocol, data models │
│ - No platform APIs │
└────────────┬────────────────────────────────┘
│
├──────────────────────┬────────────────────
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ jvmAndroid │ │ iosMain │
│ JVM libs shared │ │ iOS common │
│ - Jackson │ │ │
│ - OkHttp │ └────┬─────────────┘
└───┬───────────┬───┘ │
│ │ │
▼ ▼ ├─→ iosArm64Main
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
│android │ │jvmMain │
│Main │ │(Desktop) │
└─────────┘ └──────────┘
Future: jsMain, wasmMain
**关键要点:**jvmAndroid 并非一个平台,而是共享的 JVM 层。
jvmAndroid 模式
**Amethyst 独有。**在 Android 和 Desktop 之间共享 JVM 库。
何时使用 jvmAndroid
以下情况使用 jvmAndroid:
- ✅ 使用特定于 JVM 的库(Jackson、OkHttp、url-detector)
- ✅ Android 实现与 Desktop 实现相同(都使用 JVM)
- ✅ 库不适用于 iOS/web
以下情况不要使用 jvmAndroid:
- ❌ 纯 Kotlin 代码(使用 commonMain)
- ❌ 平台专属 API(使用 androidMain/jvmMain)
- ❌ 应该适用于所有平台的代码
来自 quartz/build.gradle.kts 的示例
// Must be defined BEFORE androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
api(libs.jackson.module.kotlin) // JSON parsing - JVM only
api(libs.url.detector) // URL extraction - JVM only
implementation(libs.okhttp) // HTTP client - JVM only
}
}
// Both depend on jvmAndroid
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
为什么 Jackson 放在 jvmAndroid,而非 commonMain?
- Jackson 是特定于 JVM 的库
- 可在 Android 上运行(运行于 JVM)
- 可在 Desktop 上运行(运行于 JVM)
- 不能在 iOS(非 JVM)或 web(非 JVM)上运行
**Web/wasm 注意事项:**为未来支持 web,可考虑将 Jackson 迁移为 kotlinx.serialization(参见“按目标平台提供的指导”)。
应抽象的内容与应保留为平台专属的内容
根据代码库模式快速判断:
始终抽象
- 加密(Secp256k1、加密、签名)
- 核心协议逻辑(Nostr 事件、NIPs)
- **原因:**所有平台都需要,且各平台的安全 API 不同
通常抽象
- I/O 操作(文件读取、缓存)
- 日志记录(各平台的日志系统不同)
- 序列化(如果使用 kotlinx.serialization)
- **原因:**通常会复用,且有可用的平台实现
有时抽象
- **业务逻辑:**是——状态机、数据处理
- **ViewModel:**是——状态和业务逻辑可以共享(StateFlow/SharedFlow)
- **屏幕布局:**是——在
commonsUI中共享;它们根据窗口大小而非平台进行适配 - **原因:**Android(从手机到笔记本电脑)和 Desktop 共用一套 UI;只有入口点不同
很少抽象
- 复杂 UI 组件(依赖大量平台功能的 composable)
- **原因:**各平台范式可能差异很大
绝不抽象
- 进程/窗口入口点(Activity、Service、Window、托盘)
- 权限(Android 与 iOS 的 API 不兼容)
- 平台 UX 模式
- **原因:**平台专属性太强,抽象会产生泄漏的 API
shared-ui-analysis.md 中的证据
| 组件 | 共享? | 理由 |
|---|---|---|
| PubKeyFormatter、ZapFormatter | ✅ 是 | 纯 Kotlin,不依赖平台 API |
| TimeAgoFormatter | ⚠️ 已抽象 | 需要用于本地化字符串的 StringProvider |
| ViewModel(状态 + 逻辑) | ✅ 是 | StateFlow/SharedFlow 与平台无关,兼容 Compose Multiplatform 生命周期 |
| 屏幕布局(Scaffold、导航) | ✅ 是(迁移中) | 使用同一套 UI;ScreenLayoutSpec 会根据窗口大小选择底部栏、侧边栏或抽屉 |
| 图片加载(Coil) | ⚠️ 已抽象 | Coil 3.x 支持 KMP,需要 expect/actual 封装器 |
expect/actual 机制
适用场景: 代码需供 2 个或更多平台使用,但会因平台而异。
代码库中的模式类别
对象(单例):
// 24 expect declarations found, common pattern:
expect object Secp256k1Instance { ... }
expect object Log { ... }
expect object LibSodiumInstance { ... }
类(可实例化):
expect class AESCBC { ... }
expect class DigestInstance { ... }
函数(工具函数):
expect fun platform(): String
expect fun currentTimeSeconds(): Long
完整目录及其理由请参阅 references/expect-actual-catalog.md。
特定目标平台指南
Android、JVM(桌面)、iOS - 当前主要目标平台
状态: 模式成熟,API 稳定
Android(androidMain):
- 使用 Android 框架(Activity、Context 等)
- 使用 secp256k1-kmp-jni-android(
libs.versions.toml中的0.23.0)进行加密 - AndroidX 库
桌面 JVM(jvmMain):
- 使用 Compose Desktop(Window、MenuBar 等)
- 使用 secp256k1-kmp-jni-jvm(同为
0.23.0版本线)进行加密 - 纯 JVM 库
iOS(iosMain):
- 成熟的目标平台 — 正在积极构建和测试
- 架构目标:iosArm64、iosSimulatorArm64、iosX64(以及用于主机工具的 macosArm64)
- 通过 platform.posix 和 Security 框架使用平台 API
Web、wasm - 未来目标平台
状态: 尚未实现,请考虑为未来兼容性做好准备
需了解的限制:
- ❌ 不支持 platform.posix(文件 I/O 方式不同)
- ❌ 不支持 JVM 库(Jackson、OkHttp 无法使用)
- ❌ 异步模型不同(JS 事件循环与线程不同)
面向未来的建议:
- 尽量在 commonMain 中使用纯 Kotlin
- 使用 kotlinx.* 库:
- 使用 kotlinx.serialization 替代 Jackson
- 使用 ktor 替代 OkHttp(ktor 支持 Web)
- 使用 kotlinx.datetime 替代自定义日期处理
- 文件操作避免使用 platform.posix
- 测试抽象层在不依赖 JVM 假设的情况下能否正常工作
迁移路径示例:
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
集成:何时调用其他技能
调用 gradle-expert
遇到以下情况时,触发 gradle-expert 技能:
- 依赖冲突(例如,secp256k1-android 与 secp256k1-jvm 版本不匹配)
- 与源集相关的构建错误
- 版本目录问题(libs.versions.toml)
- “Duplicate class”错误
- 性能或构建时间问题
触发示例:
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
→ 调用 gradle-expert 解决依赖冲突。
需要指出的问题
commonMain 中的平台代码:
// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context // Context is Android-only!
→ 指出:“commonMain 中的 Android API 无法在其他平台上编译”
重复的业务逻辑:
// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // Duplicated!
→ 指出:“业务逻辑存在重复,应放入 commonMain 或使用 expect/actual”
避免重复造轮子 - 建议使用 KMP 替代方案:
- 自定义日期/时间处理 → kotlinx.datetime
- OkHttp → ktor(支持 Web)
- Jackson → kotlinx.serialization
- 自定义 UUID → kotlinx.uuid(稳定后使用)
常见陷阱
1. 过度抽象
问题: 为 UI 组件创建 expect/actual
// ❌ BAD
expect fun NavigationComponent(...)
原因: 这会让整个导航 UI 在每个平台上各自分叉。
解决方案: 在 commonsUI 中只编写一次导航;仅将平台特有的末端功能
(Activity 结果、文件选择器)放在 expect/actual 后面或插槽中
2. 共享不足
问题: 在各个平台重复业务逻辑
// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }
原因: 错误修复需要应用两次,测试也会重复 解决方案: 移至 commonMain(纯 Kotlin),或创建 expect/actual
3. 抽象泄漏
问题: commonMain 中的平台代码
// commonMain - ❌ BAD
import android.content.Context // Won't compile on iOS!
解决方案: 使用 expect/actual 或依赖注入
4. 过早抽象
问题: 在第二个平台需要之前就创建 expect/actual
// ❌ BAD - only used on Android currently
expect fun showNotification(...)
原因: 抽象边界不当,浪费精力 解决方法: 等到 iOS 确实需要时再抽象
5. 错误的源集
问题: 在 commonMain 中使用 JVM 库
// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper
原因: Jackson 无法在 iOS/web 上编译 解决方法: 移至 jvmAndroid,或迁移至 kotlinx.serialization
快速参考
| 代码类型 | 建议位置 | 原因 |
|---|---|---|
| 纯 Kotlin 业务逻辑 | commonMain | 可在所有平台运行 |
| Nostr 协议、NIPs | commonMain | 核心逻辑,不使用平台 API |
| JVM 库(Jackson、OkHttp) | jvmAndroid | 仅限 Android + Desktop |
| 加密(因平台而异) | commonMain 中的 expect,平台中的 actual | 各平台的安全 API 不同 |
| I/O、日志记录 | commonMain 中的 expect,平台中的 actual | 各平台实现不同 |
| 状态(业务逻辑) | commonMain 或 commons/jvmAndroid | 可复用的 StateFlow 模式 |
| ViewModels | commons/commonMain/viewmodels/ | StateFlow/SharedFlow + 逻辑可共享,兼容 Compose MP 生命周期 |
| UI 格式化器(纯) | commons/commonMain | 可复用,无依赖 |
| UI 组件(简单) | commonsUI/commonMain | 卡片、按钮、对话框(Compose UI 不放在 commons 中) |
| 屏幕布局 | commonsUI | 使用 ScreenLayoutSpec 根据窗口大小调整同一套 UI |
| 导航 | commonsUI(入口由原生端提供) | 导航宿主 + 界面框架可共享;由 Activity / Window 承载 |
| 权限 | 仅限平台特定代码 | API 不兼容 |
| 平台 UX(菜单等) | 仅限平台特定代码 | 需要原生体验 |
另请参阅
- references/abstraction-examples.md - 附带原因说明的良好/不良抽象示例
- references/source-set-hierarchy.md - 使用 Amethyst 示例展示的可视化层级结构
- references/expect-actual-catalog.md - 全部 24 组 expect/actual 对及其“抽象原因”
- references/target-compatibility.md - 平台限制与面向未来的设计
脚本
scripts/validate-kmp-structure.sh- 检测不正确的放置位置并验证源集scripts/suggest-kmp-dependency.sh- 推荐 KMP 库替代方案(ktor、kotlinx.serialization 等)