kotlin-multiplatform
npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatform
Agent 安装分布
Skill 文档
Kotlin Multiplatform: Platform Abstraction Decisions
Expert guidance for KMP architecture in Amethyst – deciding what to share vs keep platform-specific.
When to Use This Skill
Making platform abstraction decisions:
- “Should I create expect/actual or keep Android-only?”
- “Can I share this ViewModel logic?”
- “Where does this crypto/JSON/network implementation belong?”
- “This uses Android Context – can it be abstracted?”
- “Is this code in the wrong module?”
- Preparing for iOS/web/wasm targets
- Detecting incorrect placements
Abstraction Decision Tree
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)
Real Examples from Codebase
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:
- Android:
MainActivity(Activity + Compose Navigation) - Desktop:
Window+ sidebar + MenuBar Why: UI paradigms fundamentally different.
Mental Model: Source Sets as Dependency Graph
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 â ââââââ¬ââââââââââââââ
âââââ¬ââââââââââââ¬ââââ â
â â âââ iosX64Main
â¼ â¼ âââ iosArm64Main
âââââââââââ ââââââââââââ âââ iosSimulatorArm64Main
âandroid â âjvmMain â
âMain â â(Desktop) â
âââââââââââ ââââââââââââ
Future: jsMain, wasmMain
Key insight: jvmAndroid is NOT a platform – it’s a shared JVM layer.
The jvmAndroid Pattern
Unique to Amethyst. Shares JVM libraries between Android + Desktop.
When to Use jvmAndroid
Use jvmAndroid when:
- â JVM-specific libraries (Jackson, OkHttp, url-detector)
- â Android implementation = Desktop implementation (same JVM)
- â Library doesn’t work on iOS/web
Do NOT use jvmAndroid for:
- â Pure Kotlin code (use commonMain)
- â Platform-specific APIs (use androidMain/jvmMain)
- â Code that should work on all platforms
Example from 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) }
Why Jackson in jvmAndroid, not commonMain?
- Jackson is JVM-specific library
- Works on Android (runs on JVM)
- Works on Desktop (runs on JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
Web/wasm consideration: For future web support, consider migrating from Jackson â kotlinx.serialization (see Target-Specific Guidance).
What to Abstract vs Keep Platform-Specific
Quick decision guidelines based on codebase patterns:
Always Abstract
- Crypto (Secp256k1, encryption, signing)
- Core protocol logic (Nostr events, NIPs)
- Why: Needed everywhere, platform security APIs vary
Often Abstract
- I/O operations (file reading, caching)
- Logging (platform logging systems differ)
- Serialization (if using kotlinx.serialization)
- Why: Commonly reused, platform implementations available
Sometimes Abstract
- Business logic: YES – state machines, data processing
- ViewModels: YES – state + business logic shareable (StateFlow/SharedFlow)
- Screen layouts: NO – platform-native (Window vs Activity)
- Why: ViewModels contain platform-agnostic state; Screens render differently per platform
Rarely Abstract
- Complex UI components (composables with heavy platform dependencies)
- Why: Platform paradigms can differ significantly
Never Abstract
- Navigation (Activity vs Window fundamentally different)
- Permissions (Android vs iOS APIs incompatible)
- Platform UX patterns
- Why: Too platform-specific, abstraction creates leaky APIs
Evidence from shared-ui-analysis.md
| 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 |
expect/actual Mechanics
When to use: Code needed by 2+ platforms, varies by platform.
Pattern Categories from Codebase
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.
Target-Specific Guidance
Android, JVM (Desktop), iOS – Current Primary Targets
Status: Mature patterns, stable APIs
Android (androidMain):
- Uses Android framework (Activity, Context, etc.)
- secp256k1-kmp-jni-android for crypto
- AndroidX libraries
Desktop JVM (jvmMain):
- Uses Compose Desktop (Window, MenuBar, etc.)
- secp256k1-kmp-jni-jvm for crypto
- Pure JVM libraries
iOS (iosMain):
- Active development, framework configured
- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main
- Platform APIs via platform.posix, Security framework
Web, wasm – Future Targets
Status: Not yet implemented, consider for future-proofing
Constraints to know:
- â No platform.posix (file I/O different)
- â No JVM libraries (Jackson, OkHttp won’t work)
- â Different async model (JS event loop vs threads)
Future-proofing tips:
- Prefer pure Kotlin in commonMain
- Use kotlinx.* libraries:
- kotlinx.serialization instead of Jackson
- ktor instead of OkHttp (ktor supports web)
- kotlinx.datetime instead of custom date handling
- Avoid platform.posix for file operations
- Test abstractions work without JVM assumptions
Example migration path:
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
Integration: When to Invoke Other Skills
Invoke gradle-expert
Trigger gradle-expert skill when encountering:
- Dependency conflicts (e.g., secp256k1-android vs secp256k1-jvm version mismatch)
- Build errors related to source sets
- Version catalog issues (libs.versions.toml)
- “Duplicate class” errors
- Performance/build time issues
Example trigger:
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
â Invoke gradle-expert for dependency conflict resolution.
Flags to Raise
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:
- Custom date/time â kotlinx.datetime
- OkHttp â ktor (supports web)
- Jackson â kotlinx.serialization
- Custom UUID â kotlinx.uuid (when stable)
Common Pitfalls
1. Over-Abstraction
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
2. Under-Sharing
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
3. Leaky Abstractions
Problem: Platform code in commonMain
// commonMain - â BAD
import android.content.Context // Won't compile on iOS!
Fix: Use expect/actual or dependency injection
4. Premature Abstraction
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
5. Wrong Source Set
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
Quick Reference
| 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) | 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 |
See Also
- references/abstraction-examples.md – Good/bad abstraction examples with rationale
- references/source-set-hierarchy.md – Visual hierarchy with Amethyst examples
- references/expect-actual-catalog.md – All 24 expect/actual pairs with “why abstracted”
- references/target-compatibility.md – Platform constraints and future-proofing
Scripts
scripts/validate-kmp-structure.sh– Detect incorrect placements, validate source setsscripts/suggest-kmp-dependency.sh– Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)