npx skills add ...
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform
Platform abstraction decision-making for Amethyst KMP project. Guides when to abstract vs keep platform-specific, source set placement (commonMain, jvmAndroid, platform-specific), expect/actual patterns. Covers primary targets (Android, JVM/Desktop, iOS — all mature) with web/wasm as possible future targets. Integrates with gradle-expert for dependency issues. Triggers on: abstraction decisions ("should I share this?"), source set placement questions, expect/actual creation, build.gradle.kts work, incorrect placement detection, KMP dependency suggestions.
npx skills add vitorpamplona/amethyst --skill kotlin-multiplatform
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):
Crypto → expect/actual:
Why: Each platform has different security APIs.
JSON parsing → jvmAndroid:
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.
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:
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):
Classes (instantiable):
Functions (utilities):
See references/expect-actual-catalog.md for complete catalog with rationale.
Status: Mature patterns, stable APIs
Android (androidMain):
0.23.0 in libs.versions.toml) for cryptoDesktop JVM (jvmMain):
0.23.0 line) for cryptoiOS (iosMain):
Status: Not yet implemented, consider for future-proofing
Constraints to know:
Future-proofing tips:
Example migration path:
Trigger gradle-expert skill when encountering:
Example trigger:
→ Invoke gradle-expert for dependency conflict resolution.
Platform code in commonMain:
→ Flag: "Android API in commonMain won't compile on other platforms"
Duplicated business logic:
→ Flag: "Business logic duplicated, should be in commonMain or expect/actual"
Reinventing wheel - suggest KMP alternatives:
Problem: Creating expect/actual for UI components
Why: Navigation paradigms too different (Activity vs Window) Fix: Keep platform-specific, accept duplication
Problem: Duplicating business logic across platforms
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
Fix: Use expect/actual or dependency injection
Problem: Creating expect/actual before second platform needs it
Why: Wrong abstraction boundaries, wasted effort Fix: Wait until iOS actually needs it, then abstract
Problem: JVM libraries in commonMain
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 | Reusable, no dependencies |
| UI components (simple) | commonsUI/commonMain | Cards, buttons, dialogs (Compose UI never goes in commons) |
| 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 |
scripts/validate-kmp-structure.sh - Detect incorrect placements, validate source setsscripts/suggest-kmp-dependency.sh - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)*// 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// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}┌─────────────────────────────────────────────┐
│ 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// 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) }// 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// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context // Context is Android-only!// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // Duplicated!// ❌ BAD
expect fun NavigationComponent(...)// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }// commonMain - ❌ BAD
import android.content.Context // Won't compile on iOS!// ❌ BAD - only used on Android currently
expect fun showNotification(...)// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper