PluginBench
Skill
Review
Audit score 70

kotlin-multiplatform

vitorpamplona/amethyst

Platform abstraction guidance for Kotlin Multiplatform projects—decide what to share across Android, iOS, and Desktop.

What is kotlin-multiplatform?

This skill provides decision-making frameworks for Kotlin Multiplatform (KMP) architecture in the Amethyst project. It guides when to abstract code into commonMain, use expect/actual patterns, leverage the jvmAndroid source set, or keep code platform-specific. Use it when deciding whether to share logic across platforms, placing code in the correct source set, or preparing for new targets like iOS or web.

  • Provides a decision tree to determine whether code should be shared across platforms or kept platform-specific
  • Explains source set placement (commonMain, jvmAndroid, androidMain, jvmMain, iosMain) with concrete examples
  • Documents the jvmAndroid pattern for sharing JVM libraries between Android and Desktop targets
  • Guides expect/actual pattern creation for platform-varying implementations like crypto and logging
  • Detects incorrect code placement and suggests refactoring
  • Covers abstraction trade-offs: when maintenance cost justifies abstraction versus duplication

How to install kotlin-multiplatform

npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatform
Claude Code
Cursor
Windsurf
Cline

How to use kotlin-multiplatform

  1. 1.When facing an abstraction decision, follow the decision tree: Is it used by 2+ platforms? Is it pure Kotlin? Does it vary by platform or JVM vs non-JVM?
  2. 2.For code used by multiple platforms, check if it's pure Kotlin (commonMain) or platform-specific (expect/actual or jvmAndroid)
  3. 3.For JVM-specific libraries like Jackson or OkHttp, place them in jvmAndroid source set so both Android and Desktop can use them
  4. 4.For platform-varying implementations (crypto, logging, file I/O), create expect declarations in commonMain and actual implementations in androidMain, jvmMain, iosMain
  5. 5.Review the source set dependency graph to ensure code flows from commonMain → jvmAndroid/iosMain → platform-specific leaves (androidMain, jvmMain)

Use cases

Good for
  • Deciding whether to create expect/actual for a crypto function or keep Android-only implementation
  • Determining if a ViewModel's business logic can be shared across Android, iOS, and Desktop
  • Choosing between jvmAndroid (for Jackson JSON parsing) versus commonMain for serialization
  • Planning migration from JVM-specific libraries (Jackson) to multiplatform alternatives (kotlinx.serialization) for future web support
  • Reviewing code placement in a KMP project and identifying modules in wrong source sets
Who it's for
  • Kotlin Multiplatform developers building cross-platform apps
  • Android developers expanding to iOS or Desktop targets
  • Architects designing KMP project structure and abstraction layers
  • Teams maintaining the Amethyst Nostr client or similar KMP projects

kotlin-multiplatform FAQ

Should I put pure Kotlin business logic in commonMain or a platform-specific source set?

Always put pure Kotlin code (no platform APIs) in commonMain. It's reusable across all platforms and simplifies maintenance. Only move to platform-specific source sets if it depends on platform APIs or libraries.

What is jvmAndroid and when should I use it?

jvmAndroid is a shared source set for JVM-specific libraries (Jackson, OkHttp, url-detector) that work on both Android and Desktop but not iOS or web. Use it when a library is JVM-only and needed by both Android and Desktop implementations.

How do I decide between expect/actual and keeping code platform-specific?

Use expect/actual when code is needed by 2+ platforms but varies by platform (e.g., crypto APIs differ between Android, iOS, Desktop). Keep code platform-specific if only one platform uses it. If abstraction cost exceeds duplication cost, duplicate instead.

Can I share ViewModels and navigation logic across Android, iOS, and Desktop?

Yes. ViewModels with StateFlow/SharedFlow and navigation logic are platform-agnostic and can be shared in commonMain or commonsUI. Only the entry points (Activity, Window, tray) remain platform-specific.

Should I migrate from Jackson to kotlinx.serialization for future web support?

Yes, for future web/wasm targets. Jackson is JVM-only and won't work on web. kotlinx.serialization is multiplatform and should be used in commonMain for code that needs to support all platforms including web.

Full instructions (SKILL.md)

Source of truth, from vitorpamplona/amethyst.


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 — 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.

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
   → 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)

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 → shared, entry points → platform-specific:

  • Shared (commonsUI, moving there): the nav host, bottom bar / rail / permanent drawer, chosen by window size via ScreenLayoutSpec
  • Android: MainActivity hosts it; Desktop: a Window (+ MenuBar, tray) hosts it Why: Android runs on laptops too, and the new Desktop app renders the same UI. See commons/plans/2026-09-27-one-ui-android-desktop.md. Today's desktopApp sidebar shell is legacy.

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          │  └────┬─────────────┘
   └───┬───────────┬───┘       │
       │           │           │
       ▼           ▼           ├─→ 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: YES - shared in commonsUI; they adapt to window size, not platform
  • Why: One UI for Android (phones to laptops) and Desktop; only entry points differ

Rarely Abstract

  • Complex UI components (composables with heavy platform dependencies)
  • Why: Platform paradigms can differ significantly

Never Abstract

  • Process / window entry points (Activity, Service, Window, tray)
  • Permissions (Android vs iOS APIs incompatible)
  • Platform UX patterns
  • Why: Too platform-specific, abstraction creates leaky APIs

Evidence from shared-ui-analysis.md

ComponentShared?Rationale
PubKeyFormatter, ZapFormatter✅ YESPure Kotlin, no platform APIs
TimeAgoFormatter⚠️ ABSTRACTEDNeeds StringProvider for localized strings
ViewModels (state + logic)✅ YESStateFlow/SharedFlow platform-agnostic, Compose Multiplatform lifecycle compatible
Screen layouts (Scaffold, nav)✅ YES (moving)One UI; ScreenLayoutSpec picks bottom bar / rail / drawer by window size
Image loading (Coil)⚠️ ABSTRACTEDCoil 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 (0.23.0 in libs.versions.toml) for crypto
  • AndroidX libraries

Desktop JVM (jvmMain):

  • Uses Compose Desktop (Window, MenuBar, etc.)
  • secp256k1-kmp-jni-jvm (same 0.23.0 line) for crypto
  • Pure JVM libraries

iOS (iosMain):

  • Mature target — actively built and tested
  • Architecture targets: iosArm64, iosSimulatorArm64, iosX64 (plus macosArm64 for host tooling)
  • 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:

// 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: It forks the whole navigation UI per platform. Fix: Write the navigation once in commonsUI; put only the platform leaf (an Activity result, a file picker) behind expect/actual or a slot

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 TypeRecommended LocationReason
Pure Kotlin business logiccommonMainWorks everywhere
Nostr protocol, NIPscommonMainCore logic, no platform APIs
JVM libs (Jackson, OkHttp)jvmAndroidAndroid + Desktop only
Crypto (varies by platform)expect in commonMain, actual in platformsDifferent security APIs per platform
I/O, loggingexpect in commonMain, actual in platformsPlatform implementations differ
State (business logic)commonMain or commons/jvmAndroidReusable StateFlow patterns
ViewModelscommons/commonMain/viewmodels/StateFlow/SharedFlow + logic shareable, Compose MP lifecycle compatible
UI formatters (pure)commons/commonMainReusable, no dependencies
UI components (simple)commonsUI/commonMainCards, buttons, dialogs (Compose UI never goes in commons)
Screen layoutscommonsUIOne UI; adapt to window size with ScreenLayoutSpec
NavigationcommonsUI (entry point native)Nav host + chrome shared; Activity / Window hosts it
PermissionsPlatform-specific onlyAPIs incompatible
Platform UX (menus, etc.)Platform-specific onlyNative feel required

See Also

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.)