Kotlin Multiplatform Architectural Guidance
Install this skill
npx skills add vitorpamplona/amethystWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
What this skill does
- β’Navigate multiplatform source set placement decisions
- β’Identify when to use commonMain versus platform-specific modules
- β’Implement the jvmAndroid pattern for shared JVM library overhead
- β’Evaluate maintainability costs versus code duplication
- β’Define clear boundaries for platform-specific crypto and networking
When to use it
- βRefactoring shared business logic into common modules
- βDetermining if a library should be scoped to jvmAndroid or commonMain
- βResolving complex dependency graph issues between Android and iOS
- βAssessing whether to implement expect/actual for hardware-specific APIs
When not to use it
- βProjects targeting only a single platform
- βRefactoring UI code that relies heavily on platform-native view hierarchies
- βSituations where simple code duplication is cheaper than creating an abstraction
How to invoke it
Example prompts that trigger this skill:
- βDecide whether to place this new module in commonMain or androidMain.β
- βExplain the pros and cons of using expect/actual for this crypto implementation.β
- βHelp me refactor these shared JVM dependencies into a jvmAndroid source set.β
- βShow me how to structure my Kotlin source sets for better reusability.β
- βDetermine if my navigation logic should be shared or kept platform-specific.β
Example workflow
- Analyze the platform dependency of the target code.
- Check the decision tree: Does this code run on more than two platforms?
- Determine if platform APIs are involved or if it is pure Kotlin.
- If JVM-exclusive, apply the jvmAndroid source set pattern.
- Define expect/actual interfaces for platform-variable security or I/O.
- Verify the final dependency graph to ensure clean separation.
Prerequisites
- βKotlin Multiplatform plugin configured
- βUnderstanding of Gradle build scripts (KTS)
- βExisting multi-target project structure
Pitfalls & limitations
- !Over-abstracting code that is inherently platform-specific
- !Polluting commonMain with code that requires platform-specific libraries
- !Neglecting the overhead of managing expect/actual pairs
FAQ
How it compares
Unlike generic KMP documentation, this skill provides a specific, codebase-proven decision tree that prioritizes architectural pragmatism over theoretical abstraction.
Source & trust
From the source: β# 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 Vieβ¦β
View the full SKILL.md source
# 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 β ββββββ¬ββββββββββββββ
βββββ¬ββββββββββββ¬ββββ β
β β β
βΌ βΌ βββ 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 (`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:**
```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.)
Quoted from vitorpamplona/amethyst for reference β see the original for the authoritative, latest version.
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/kotlin-multiplatform/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md - Cursor:
~/.cursor/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/vitorpamplona/amethyst/kotlin-multiplatform/SKILL.md
π Install with CLI:npx skills add vitorpamplona/amethyst