
Kotlin Multiplatform
FreeGuidance for platform abstraction in KMP projects.
Free · Opens the source repo
What Kotlin Multiplatform does
The Kotlin Multiplatform skill provides developers with expert guidance on making platform abstraction decisions within the Amethyst framework. It helps users determine when to share code across platforms versus keeping it platform-specific. This is particularly useful for teams working with Android, JVM/Desktop, and iOS, as it covers the nuances of source set placement, expect/actual patterns, and integration with Gradle for dependency management. The skill is designed to streamline the decision-making process, enabling developers to focus on writing efficient, maintainable code.
When using this skill, developers can easily navigate complex questions such as whether to create expect/actual declarations, where to place specific implementations, and how to prepare for future targets like web or wasm. It provides a structured decision tree that guides users through the thought process of code reuse, ensuring that they consider factors like maintenance costs and platform-specific requirements. This structured approach minimizes the risk of misplacing code and helps maintain a clean architecture.
The skill also includes practical examples from real codebases, illustrating how to apply these principles in everyday development scenarios. It addresses common challenges faced by developers, such as integrating JVM-specific libraries and managing platform-specific APIs. By leveraging this skill, teams can enhance their Kotlin Multiplatform projects' architecture and ensure that their codebase remains organized and efficient as they scale across different platforms.
When to use it
Use this skill when making decisions about code sharing and platform-specific implementations in KMP projects.
When not to use it
This skill may not be suitable for projects that do not involve Kotlin Multiplatform or for those that require a more generalized approach to coding without platform considerations.
What you can build with it
Deciding on Code Reuse
When faced with a decision on whether to abstract a ViewModel or keep it platform-specific, this skill provides a clear decision path.
Integrating JVM Libraries
If you're unsure about where to place a JVM-specific library like Jackson, this skill offers guidance on using the jvmAndroid pattern.
Preparing for iOS Targets
As you expand your project to include iOS, this skill can help you determine what code can be shared and what needs to remain platform-specific.
How to install Kotlin Multiplatform
View source1. Install with the skills CLI
npx skills add vitorpamplona/amethyst/kotlin-multiplatform --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by vitorpamplonaKotlin 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 │ └────┬─────────────┘
└───┬───────────┬───┘ │
│ │ │
▼ ▼ ├─→ 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 (
0.23.0inlibs.versions.toml) for crypto - AndroidX libraries
Desktop JVM (jvmMain):
- Uses Compose Desktop (Window, MenuBar, etc.)
- secp256k1-kmp-jni-jvm (same
0.23.0line) 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:
- 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.)
Frequently asked questions about Kotlin Multiplatform
Similar skills
Android App Development
Comprehensive guide for Android and cross-platform app development.
Add App Clip to Expo App
Integrate lightweight iOS App Clips into your Expo project.
APK Reverse
Streamline your Android APK reverse engineering process.
Swift Expert
Master iOS/macOS development with Swift and SwiftUI.
React Native Expert
Build and optimize mobile apps with React Native and Expo.
Kotlin Specialist
Master idiomatic Kotlin with expert patterns and practices.
