mcpField
K
kotlin-multiplatform

kotlin-multiplatform

github.com/vitorpamplona开发1.6k1.6k 次安装今天更新
代码仓库

关于 kotlin-multiplatform

kotlin-multiplatform 是面向 Amethyst 中 Kotlin Multiplatform 架构决策的指导技能。它帮助判断代码应放入共享源集还是保留平台专属,并提供代码库示例及当前和未来目标平台的建议。

主要功能

  1. 01判断共享代码与平台专属代码
  2. 02源集依赖关系指导
  3. 03expect/actual 使用模式
  4. 04JVM 与目标平台兼容性建议
  5. 05常见抽象设计问题

适用场景

  1. 01为新代码选择源集
  2. 02检查 commonMain 中的平台 API
  3. 03规划 Android、Desktop 和 iOS 间的代码共享
  4. 04评估面向未来 web 或 wasm 目标的抽象

安装

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

$npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatform

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

包含的文件

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

references/
abstraction-examples.md
expect-actual-catalog.md
source-set-hierarchy.md
target-compatibility.md
scripts/
suggest-kmp-dependency.sh
validate-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 事件循环与线程不同)

面向未来的建议:

  1. 尽量在 commonMain 中使用纯 Kotlin
  2. 使用 kotlinx.* 库:
    • 使用 kotlinx.serialization 替代 Jackson
    • 使用 ktor 替代 OkHttp(ktor 支持 Web)
    • 使用 kotlinx.datetime 替代自定义日期处理
  3. 文件操作避免使用 platform.posix
  4. 测试抽象层在不依赖 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 协议、NIPscommonMain核心逻辑,不使用平台 API
JVM 库(Jackson、OkHttp)jvmAndroid仅限 Android + Desktop
加密(因平台而异)commonMain 中的 expect,平台中的 actual各平台的安全 API 不同
I/O、日志记录commonMain 中的 expect,平台中的 actual各平台实现不同
状态(业务逻辑)commonMain 或 commons/jvmAndroid可复用的 StateFlow 模式
ViewModelscommons/commonMain/viewmodels/StateFlow/SharedFlow + 逻辑可共享,兼容 Compose MP 生命周期
UI 格式化器(纯)commons/commonMain可复用,无依赖
UI 组件(简单)commonsUI/commonMain卡片、按钮、对话框(Compose UI 不放在 commons 中)
屏幕布局commonsUI使用 ScreenLayoutSpec 根据窗口大小调整同一套 UI
导航commonsUI(入口由原生端提供)导航宿主 + 界面框架可共享;由 Activity / Window 承载
权限仅限平台特定代码API 不兼容
平台 UX(菜单等)仅限平台特定代码需要原生体验

另请参阅

脚本

  • scripts/validate-kmp-structure.sh - 检测不正确的放置位置并验证源集
  • scripts/suggest-kmp-dependency.sh - 推荐 KMP 库替代方案(ktor、kotlinx.serialization 等)

评论

支持 Markdown