kotlin-multiplatform by vitorpamplona/amethyst
npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatformAmethyst 中 KMP 架构的专家指导 - 决定共享哪些内容与保留平台特定内容。
进行平台抽象决策时:
核心问题: "这段代码是否需要在多个平台间复用?"
遵循此决策路径(< 1 分钟):
Q: 它被 2 个或更多平台使用吗?
├─ 否 → 保持平台特定
│ 示例:仅 Android 的权限处理
│
└─ 是 → 继续 ↓
Q: 它是纯 Kotlin 代码(无平台 API)吗?
├─ 是 → commonMain
│ 示例:Nostr 事件解析、业务规则
│
└─ 否 → 继续 ↓
Q: 它因平台而异还是因 JVM 与非 JVM 而异?
├─ 因平台而异(Android ≠ iOS ≠ Desktop)
│ → expect/actual
│ 示例:Secp256k1Instance(使用不同的安全 API)
│
├─ 因 JVM 而异(Android = Desktop ≠ iOS/web)
│ → jvmAndroid
│ 示例:Jackson JSON 解析(JVM 库)
│
└─ 复杂/UI 相关
→ 保持平台特定
示例:导航(Activity 与 Window 差异太大)
最终检查:
Q: 抽象化的维护成本 < 重复代码的成本吗?
├─ 是 → 进行抽象化
└─ 否 → 重复代码(更简单)
加密 → expect/actual:
// commonMain - expect 声明
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}
// androidMain - 使用 Android Keystore
// jvmMain - 使用桌面 JVM 加密
// iosMain - 使用 iOS Security 框架
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
原因: 每个平台有不同的安全 API。
JSON 解析 → jvmAndroid:
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}
原因: Jackson 是 JVM 专用的,在 Android + Desktop 上工作,不在 iOS/web 上。
导航 → 平台特定:
MainActivity(Activity + Compose Navigation)Window + 侧边栏 + MenuBar 原因: UI 范式根本不同。将源集视为依赖图,而非文件夹。
┌─────────────────────────────────────────────┐
│ commonMain = 契约(纯 Kotlin) │
│ - 业务逻辑、协议、数据模型 │
│ - 无平台 API │
└────────────┬────────────────────────────────┘
│
├──────────────────────┬────────────────────
│ │
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ jvmAndroid │ │ iosMain │
│ JVM 库共享 │ │ iOS 通用 │
│ - Jackson │ │ │
│ - OkHttp │ └────┬─────────────┘
└───┬───────────┬───┘ │
│ │ │
▼ ▼ ├─→ iosArm64Main
┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main
│android │ │jvmMain │
│Main │ │(Desktop) │
└─────────┘ └──────────┘
未来:jsMain, wasmMain
关键见解: jvmAndroid 不是一个平台 - 它是一个共享的 JVM 层。
Amethyst 独有。 在 Android 和 Desktop 之间共享 JVM 库。
在以下情况下使用 jvmAndroid:
不要将 jvmAndroid 用于:
// 必须在 androidMain 和 jvmMain 之前定义
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
api(libs.jackson.module.kotlin) // JSON 解析 - 仅 JVM
api(libs.url.detector) // URL 提取 - 仅 JVM
implementation(libs.okhttp) // HTTP 客户端 - 仅 JVM
}
}
// 两者都依赖 jvmAndroid
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
为什么 Jackson 在 jvmAndroid 中,而不是 commonMain?
Web/wasm 考虑: 为了未来的 web 支持,考虑从 Jackson 迁移到 kotlinx.serialization(参见目标平台特定指导)。
基于代码库模式的快速决策指南:
| 组件 | 共享? | 理由 |
|---|---|---|
| PubKeyFormatter, ZapFormatter | ✅ 是 | 纯 Kotlin,无平台 API |
| TimeAgoFormatter | ⚠️ 已抽象化 | 需要 StringProvider 用于本地化字符串 |
| ViewModels(状态 + 逻辑) | ✅ 是 | StateFlow/SharedFlow 平台无关,与 Compose Multiplatform 生命周期兼容 |
| 屏幕布局(Scaffold, nav) | ❌ 否 | Window 与 Activity,侧边栏与底部导航根本不同 |
| 图像加载(Coil) | ⚠️ 已抽象化 | Coil 3.x 支持 KMP,需要 expect/actual 包装器 |
何时使用: 被 2 个或更多平台需要的代码,且因平台而异。
对象(单例):
// 找到 24 个 expect 声明,常见模式:
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 获取包含理由的完整目录。
状态: 成熟的模式,稳定的 API
Android(androidMain):
Desktop JVM(jvmMain):
iOS(iosMain):
状态: 尚未实现,为未来兼容性考虑
需要了解的约束:
未来兼容性提示:
示例迁移路径:
// 当前:jvmAndroid(仅 JVM)
api(libs.jackson.module.kotlin)
// 未来:commonMain(所有平台)
api(libs.kotlinx.serialization.json)
在遇到以下情况时触发 gradle-expert 技能:
示例触发:
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
→ 调用 gradle-expert 解决依赖冲突。
commonMain 中的平台代码:
// ❌ 错误 - commonMain 中的 Android API
expect fun getContext(): Context // Context 是仅 Android 的!
→ 标记:"commonMain 中的 Android API 在其他平台上无法编译"
重复的业务逻辑:
// ❌ 错误 - 两者中相同的逻辑
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // 重复!
→ 标记:"业务逻辑重复,应放在 commonMain 或 expect/actual 中"
重新发明轮子 - 建议 KMP 替代方案:
问题: 为 UI 组件创建 expect/actual
// ❌ 错误
expect fun NavigationComponent(...)
原因: 导航范式差异太大(Activity 与 Window) 修复: 保持平台特定,接受重复
问题: 跨平台重复业务逻辑
// ❌ 错误 - 在 androidMain 和 jvmMain 中重复
fun parseNostrEvent(json: String): Event { ... }
原因: 错误修复需要应用两次,测试重复 修复: 移动到 commonMain(纯 Kotlin)或创建 expect/actual
问题: commonMain 中的平台代码
// commonMain - ❌ 错误
import android.content.Context // 在 iOS 上无法编译!
修复: 使用 expect/actual 或依赖注入
问题: 在第二个平台需要之前创建 expect/actual
// ❌ 错误 - 目前仅在 Android 上使用
expect fun showNotification(...)
原因: 错误的抽象边界,浪费精力 修复: 等到 iOS 实际需要时再抽象
问题: commonMain 中的 JVM 库
// commonMain - ❌ 错误
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 组件(简单) | commons/commonMain | 卡片、按钮、对话框 |
| 屏幕布局 | 平台特定 | Window 与 Activity,侧边栏与底部导航 |
| 导航 | 仅平台特定 | Activity 与 Window 差异太大 |
| 权限 | 仅平台特定 | API 不兼容 |
| 平台 UX(菜单等) | 仅平台特定 | 需要原生感觉 |
scripts/validate-kmp-structure.sh - 检测错误的放置位置,验证源集scripts/suggest-kmp-dependency.sh - 建议 KMP 库替代方案(ktor、kotlinx.serialization 等)每周安装次数
241
仓库
GitHub 星标数
1.5K
首次出现
Jan 21, 2026
安全审计
安装于
opencode213
gemini-cli208
codex204
github-copilot197
amp180
cursor179
Expert guidance for KMP architecture in Amethyst - deciding what to share vs keep platform-specific.
Making platform abstraction decisions:
Central question: "Should this code be reused across platforms?"
Follow this decision path (< 1 minute):
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
→ Keep platform-specific
Example: Navigation (Activity vs Window too different)
Final check:
Q: Maintenance cost of abstraction < duplication cost?
├─ YES → Proceed with abstraction
└─ NO → Duplicate (simpler)
Crypto → 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
Why: Each platform has different security APIs.
JSON parsing → jvmAndroid:
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}
Why: Jackson is JVM-only, works on Android + Desktop, not iOS/web.
Navigation → platform-specific:
MainActivity (Activity + Compose Navigation)Window + sidebar + MenuBar Why: UI paradigms fundamentally different.Think of source sets as a dependency graph, not folders.
┌─────────────────────────────────────────────┐
│ 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
Key insight: jvmAndroid is NOT a platform - it's a shared JVM layer.
Unique to Amethyst. Shares JVM libraries between Android + Desktop.
Use jvmAndroid when:
Do NOT use jvmAndroid for:
// 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) }
Why Jackson in jvmAndroid, not commonMain?
Web/wasm consideration: For future web support, consider migrating from Jackson → kotlinx.serialization (see Target-Specific Guidance).
Quick decision guidelines based on codebase patterns:
| Component | Shared? | Rationale |
|---|---|---|
| PubKeyFormatter, ZapFormatter | ✅ YES | Pure Kotlin, no platform APIs |
| TimeAgoFormatter | ⚠️ ABSTRACTED | Needs StringProvider for localized strings |
| ViewModels (state + logic) | ✅ YES | StateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible |
| Screen layouts (Scaffold, nav) | ❌ NO | Window vs Activity, sidebar vs bottom nav fundamentally different |
| Image loading (Coil) | ⚠️ ABSTRACTED | Coil 3.x supports KMP, needs expect/actual wrapper |
When to use: Code needed by 2+ platforms, varies by platform.
Objects (singletons):
// 24 expect declarations found, common pattern:
expect object Secp256k1Instance { ... }
expect object Log { ... }
expect object LibSodiumInstance { ... }
Classes (instantiable):
expect class AESCBC { ... }
expect class DigestInstance { ... }
Functions (utilities):
expect fun platform(): String
expect fun currentTimeSeconds(): Long
See references/expect-actual-catalog.md for complete catalog with rationale.
Status: Mature patterns, stable APIs
Android (androidMain):
Desktop JVM (jvmMain):
iOS (iosMain):
Status: Not yet implemented, consider for future-proofing
Constraints to know:
Future-proofing tips:
Example migration path:
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
Trigger gradle-expert skill when encountering:
Example trigger:
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
→ Invoke gradle-expert for dependency conflict resolution.
Platform code in commonMain:
// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context // Context is Android-only!
→ Flag: "Android API in commonMain won't compile on other platforms"
Duplicated business logic:
// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // Duplicated!
→ Flag: "Business logic duplicated, should be in commonMain or expect/actual"
Reinventing wheel - suggest KMP alternatives:
Problem: Creating expect/actual for UI components
// ❌ BAD
expect fun NavigationComponent(...)
Why: Navigation paradigms too different (Activity vs Window) Fix: Keep platform-specific, accept duplication
Problem: Duplicating business logic across platforms
// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }
Why: Bug fixes need to be applied twice, tests duplicated Fix: Move to commonMain (pure Kotlin) or create expect/actual
Problem: Platform code in commonMain
// commonMain - ❌ BAD
import android.content.Context // Won't compile on iOS!
Fix: Use expect/actual or dependency injection
Problem: Creating expect/actual before second platform needs it
// ❌ BAD - only used on Android currently
expect fun showNotification(...)
Why: Wrong abstraction boundaries, wasted effort Fix: Wait until iOS actually needs it, then abstract
Problem: JVM libraries in commonMain
// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper
Why: Jackson won't compile on iOS/web Fix: Move to jvmAndroid or migrate to kotlinx.serialization
| Code Type | Recommended Location | Reason |
|---|---|---|
| Pure Kotlin business logic | commonMain | Works everywhere |
| Nostr protocol, NIPs | commonMain | Core logic, no platform APIs |
| JVM libs (Jackson, OkHttp) | jvmAndroid | Android + Desktop only |
| Crypto (varies by platform) | expect in commonMain, actual in platforms | Different security APIs per platform |
| I/O, logging | expect in commonMain, actual in platforms | Platform implementations differ |
| State (business logic) | commonMain or commons/jvmAndroid | Reusable StateFlow patterns |
| ViewModels | commons/commonMain/viewmodels/ | StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible |
| UI formatters (pure) | commons/commonMain |
scripts/validate-kmp-structure.sh - Detect incorrect placements, validate source setsscripts/suggest-kmp-dependency.sh - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)Weekly Installs
241
Repository
GitHub Stars
1.5K
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode213
gemini-cli208
codex204
github-copilot197
amp180
cursor179
Kotlin Ktor 服务器模式指南:构建健壮 HTTP API 的架构与最佳实践
754 周安装
| Reusable, no dependencies |
| UI components (simple) | commons/commonMain | Cards, buttons, dialogs |
| Screen layouts | Platform-specific | Window vs Activity, sidebar vs bottom nav |
| Navigation | Platform-specific only | Activity vs Window too different |
| Permissions | Platform-specific only | APIs incompatible |
| Platform UX (menus, etc.) | Platform-specific only | Native feel required |