--- name: kotlin-multiplatform description: | 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) with web/wasm future considerations. 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. --- # 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:** ```kotlin // 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:** ```kotlin // 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 ```kotlin // 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):** ```kotlin // 24 expect declarations found, common pattern: expect object Secp256k1Instance { ... } expect object Log { ... } expect object LibSodiumInstance { ... } ``` **Classes (instantiable):** ```kotlin expect class AESCBC { ... } expect class DigestInstance { ... } ``` **Functions (utilities):** ```kotlin expect fun platform(): String expect fun currentTimeSeconds(): Long ``` **See** [references/expect-actual-catalog.md](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:** 1. Prefer pure Kotlin in commonMain 2. Use kotlinx.* libraries: - kotlinx.serialization instead of Jackson - ktor instead of OkHttp (ktor supports web) - kotlinx.datetime instead of custom date handling 3. Avoid platform.posix for file operations 4. Test abstractions work without JVM assumptions **Example migration path:** ```kotlin // 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:** ```kotlin // ❌ 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:** ```kotlin // ❌ 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 ```kotlin // ❌ 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 ```kotlin // ❌ 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 ```kotlin // 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 ```kotlin // ❌ 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 ```kotlin // 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](references/abstraction-examples.md) - Good/bad abstraction examples with rationale - [references/source-set-hierarchy.md](references/source-set-hierarchy.md) - Visual hierarchy with Amethyst examples - [references/expect-actual-catalog.md](references/expect-actual-catalog.md) - All 24 expect/actual pairs with "why abstracted" - [references/target-compatibility.md](references/target-compatibility.md) - Platform constraints and future-proofing ## Scripts - `scripts/validate-kmp-structure.sh` - Detect incorrect placements, validate source sets - `scripts/suggest-kmp-dependency.sh` - Suggest KMP library alternatives (ktor, kotlinx.serialization, etc.)