Back to list
vitorpamplona

kotlin-multiplatform

by vitorpamplona

Nostr client for Android

1,322🍴 181📅 Jan 23, 2026

SKILL.md


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:

// 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

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)❌ NOWindow vs Activity, sidebar vs bottom nav fundamentally different
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 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:

// 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 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)commons/commonMainCards, buttons, dialogs
Screen layoutsPlatform-specificWindow vs Activity, sidebar vs bottom nav
NavigationPlatform-specific onlyActivity vs Window too different
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.)

Score

Total Score

80/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 1000以上

+15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

+5
Issue管理

オープンIssueが50未満

0/5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon