
Kotlin Multiplatform
- 1.3k installs
- 1.6k repo stars
- Updated August 4, 2026
- vitorpamplona/amethyst
kotlin-multiplatform is an agent skill for Amethyst KMP abstraction and source set placement decisions.
About
The kotlin-multiplatform skill guides platform abstraction decisions for the Amethyst KMP project across Android, JVM desktop, and iOS targets with possible future web and wasm expansion. A decision tree asks whether code is used by two or more platforms, whether it is pure Kotlin, and whether variation is by platform or JVM versus non-JVM before choosing commonMain, jvmAndroid, expect/actual, or platform-specific placement. It covers incorrect placement detection, build.gradle.kts module questions, and integration with gradle-expert for dependency issues. Examples distinguish Nostr parsing for commonMain, Jackson JSON for jvmAndroid, and Secp256k1 expect/actual for platform security APIs. Maintenance cost versus duplication cost is the final check before abstracting UI or navigation code that should remain platform-specific.
- Decision tree for commonMain, jvmAndroid, expect/actual, or platform-specific code.
- Primary targets: Android, JVM desktop, and mature iOS with future web/wasm notes.
- Detects incorrect module placement and suggests KMP dependency fixes.
- Integrates with gradle-expert skill for build.gradle.kts dependency issues.
- Final abstraction check weighs maintenance cost against duplication cost.
Kotlin Multiplatform by the numbers
- 1,256 all-time installs (skills.sh)
- +49 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #191 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatformAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 1.6k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | vitorpamplona/amethyst ↗ |
Should this Amethyst code live in commonMain, jvmAndroid, or expect/actual?
Decide KMP source set placement and expect/actual abstractions in the Amethyst project.
Who is it for?
Amethyst developers adding shared logic or preparing iOS and desktop targets.
Skip if: Skip for Android-only modules with no cross-platform reuse planned.
When should I use this skill?
User asks about expect/actual, source set placement, or KMP abstraction in Amethyst.
What you get
Correct KMP module placement following the documented abstraction decision tree.
- expect/actual interface designs
- Platform rationale documentation
By the numbers
- Secp256k1Instance uses expect in commonMain with actuals in 3 platform source sets
- Examples target Android, Desktop JVM, and iOS from the Amethyst codebase
Files
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 frameworkWhy: 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, wasmMainKey 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(): LongSee 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: 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.ObjectMapperWhy: 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.)
Abstraction Examples from Amethyst Codebase
Real examples of abstraction decisions with rationale.
Good Abstractions (Why They Work)
1. Secp256k1Instance - Crypto Signing
Location: expect in commonMain, actual in androidMain/jvmMain/iosMain
Code:
// quartz/src/commonMain/.../Secp256k1Instance.kt
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
fun verifySchnorr(signature: ByteArray, hash: ByteArray, pubKey: ByteArray): Boolean
}Why abstracted:
- Used by all platforms (Android, Desktop, iOS)
- Security APIs fundamentally different:
- Android: secp256k1-kmp-jni-android (Android Keystore integration)
- Desktop: secp256k1-kmp-jni-jvm (pure JVM crypto)
- iOS: Native Security framework
- Core protocol requirement (Nostr signatures)
Decision rationale: Always abstract crypto - varies by platform security APIs, critical for all platforms.
---
2. Log - Platform Logging
Location: expect object in commonMain
Code:
// quartz/src/commonMain/.../Log.kt
expect object Log {
fun d(tag: String, message: String)
fun w(tag: String, message: String, throwable: Throwable?)
fun e(tag: String, message: String, throwable: Throwable?)
}Why abstracted:
- Used throughout quartz module (protocol library)
- Logging systems differ:
- Android: android.util.Log
- Desktop: println or logging framework
- iOS: NSLog or OSLog
- Simple interface, easy to implement
Decision rationale: Often abstract logging - platform systems differ, widely used, simple interface.
---
3. Platform Utils - Time & Platform Name
Location: expect functions in commonMain
Code:
// quartz/src/commonMain/.../Platform.kt
expect fun platform(): String
expect fun currentTimeSeconds(): LongWhy abstracted:
- Used by Nostr event creation (timestamps)
- Platform name for debugging
- Simple utilities, clear platform boundary
Decision rationale: Platform utilities are good abstraction candidates - simple, useful everywhere.
---
4. Jackson JSON (jvmAndroid Pattern)
Location: jvmAndroid source set
Code:
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin) // JVM-only library
}Why jvmAndroid (not commonMain):
- Jackson is JVM-specific library
- Works on Android (JVM) + Desktop (JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
- Performance-critical JSON parsing
Decision rationale: Use jvmAndroid for JVM libraries shared between Android and Desktop.
Future consideration: For web support, migrate to kotlinx.serialization (works on all platforms).
---
Bad/Over-Abstractions (Why They Failed)
1. Navigation Abstraction (Avoided)
What COULD have been done:
// ❌ Over-abstraction - DON'T DO THIS
expect interface Navigator {
fun navigate(route: String)
fun popBackStack()
}Why NOT abstracted:
- Navigation paradigms fundamentally different:
- Android: Activity + Compose Navigation + back stack
- Desktop: Window + screen state + no back stack concept
- Complex APIs don't map well
- Creates leaky abstraction
Actual approach: Keep platform-specific
- Android:
INavinterface + Compose Navigation - Desktop: Simple screen enum + state
Decision rationale: Never abstract navigation - platforms too different, abstraction would be leaky.
---
2. String Resources (Abstraction Planned)
Current state: Platform-specific (over-duplication)
Problem:
// Android uses R.string.*
Text(stringResource(R.string.post_not_found))
// Desktop uses hardcoded strings
Text("Post not found")Why NOT yet abstracted: Waiting for second platform to fully implement UI, then will create StringProvider interface.
Planned abstraction:
// commonMain
interface StringProvider {
fun get(key: String): String
}
// androidMain
class AndroidStringProvider(context: Context): StringProvider { ... }
// jvmMain
class DesktopStringProvider: StringProvider { ... }Lesson: Don't abstract prematurely - wait until second platform needs it, then create proper abstraction.
---
Platform-Specific Code (Why NOT Abstracted)
1. MainActivity (Android Activity)
Location: amethyst/src/main/.../MainActivity.kt
Code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
setContent {
AmethystTheme {
AccountScreen(accountStateViewModel)
}
}
}
}Why platform-specific:
- AppCompatActivity is Android framework
- Activity lifecycle unique to Android
- enableEdgeToEdge() is Android-specific API
- No equivalent on Desktop (uses Window)
Decision rationale: Android Activity is platform-specific by nature.
---
2. Desktop Window & MenuBar
Location: desktopApp/src/jvmMain/.../Main.kt
Code:
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Amethyst"
) {
MenuBar {
Menu("File") {
Item("New Note", onClick = { ... }, shortcut = KeyShortcut(Key.N, ctrl = true))
Item("Quit", onClick = ::exitApplication)
}
}
NavigationRail { ... } // Sidebar navigation
}
}Why platform-specific:
- Window, MenuBar, NavigationRail are Compose Desktop APIs
- Keyboard shortcuts (Ctrl+N) are desktop paradigm
- Sidebar navigation vs Android bottom nav
- No equivalent on Android
Decision rationale: Desktop UX patterns are platform-specific by nature.
---
3. AccountViewModel (Android ViewModel)
Location: amethyst/.../AccountStateViewModel.kt
Partially abstracted:
- Business logic → IAccountState interface (can be shared)
- UI state + lifecycle → AndroidX ViewModel (Android-only)
Why not fully abstracted:
- AndroidX ViewModel lifecycle tied to Android
- Desktop doesn't need ViewModel (simpler state management)
- SavedStateHandle is Android-specific
Decision rationale: Extract business logic to interface, keep UI state platform-specific.
---
Migration Examples (Android → Shared)
Example 1: PubKeyFormatter (Pure Kotlin)
Before:
// amethyst/ui/note/PubKeyFormatter.kt
fun String.toDisplayHexKey(): String {
return "${take(8)}:${takeLast(8)}"
}After:
// commons/commonMain/formatters/PubKeyFormatter.kt
fun String.toDisplayHexKey(): String {
return "${take(8)}:${takeLast(8)}"
}
// Both apps use it
import com.vitorpamplona.amethyst.commons.formatters.toDisplayHexKeyWhy successful:
- Pure Kotlin, no platform dependencies
- Widely reused
- Simple utility function
---
Example 2: TimeAgoFormatter (Requires Abstraction)
Problem:
// Uses Android R.string.*
fun timeAgo(timestamp: Long): String {
return context.getString(R.string.x_minutes_ago, minutes)
}Solution: Abstract string resources
// commonMain
fun timeAgo(timestamp: Long, stringProvider: StringProvider): String {
return stringProvider.get("x_minutes_ago", minutes)
}
// androidMain
stringProvider = AndroidStringProvider(context)
// jvmMain
stringProvider = DesktopStringProvider()Why successful: Clear platform boundary (string resources), useful on both platforms.
---
Decision Pattern Summary
| Pattern | Abstract? | Why |
|---|---|---|
| Pure Kotlin utilities | ✅ YES | No platform dependency, easy |
| Crypto APIs | ✅ YES (expect/actual) | Platform security APIs differ |
| JVM libraries | ⚠️ jvmAndroid | Works on Android+Desktop only |
| UI components (simple) | ✅ YES | Composables work cross-platform |
| UI components (complex) | ❌ NO | Platform dependencies |
| Navigation | ❌ NO | Paradigms too different |
| ViewModels | ⚠️ PARTIAL | Business logic yes, UI state no |
| String resources | ⚠️ PLANNED | Needs abstraction layer |
Complete expect/actual Catalog
All 24 expect declarations in Amethyst quartz module with rationale.
| # | Name | Type | Purpose | Why Abstracted | Files |
|---|---|---|---|---|---|
| 1 | AESCBC | class | AES CBC encryption | Platform crypto APIs differ | quartz/.../ciphers/AESCBC.kt |
| 2 | AESGCM | class | AES GCM encryption | Platform crypto APIs differ | quartz/.../ciphers/AESGCM.kt |
| 3 | DigestInstance | class | Hash digests (SHA256) | Platform implementations | quartz/.../diggest/DigestInstance.kt |
| 4 | MacInstance | class | MAC (HMAC) operations | Platform crypto APIs | quartz/.../mac/MacInstance.kt |
| 5 | Sha256 | object | SHA256 hashing | Platform-specific optimizations | quartz/.../sha256/Sha256.kt |
| 6 | LargeCache | object | Large object caching | Platform storage APIs differ | quartz/.../cache/LargeCache.kt |
| 7 | UriParser | object | URI parsing | Platform URL APIs differ | quartz/.../UriParser.kt |
| 8 | UrlEncoder | object | URL encoding | Platform encoding differs | quartz/.../UrlEncoder.kt |
| 9 | Urls | object | URL utilities | Platform URL handling | quartz/.../Urls.kt |
| 10 | Platform | functions | platform(), currentTimeSeconds() | Platform name & time APIs | quartz/.../Platform.kt |
| 11 | Rfc3986 | object | RFC 3986 URL normalization | Used in jvmAndroid | quartz/.../Rfc3986.kt |
| 12 | Secp256k1Instance | object | Bitcoin crypto (secp256k1) | Different libs per platform | quartz/.../Secp256k1Instance.kt |
| 13 | SecureRandom | object | Cryptographically secure random | Platform random APIs differ | quartz/.../SecureRandom.kt |
| 14 | StringExt | functions | String utilities | Platform string handling | quartz/.../StringExt.kt |
| 15 | UnicodeNormalizer | object | Unicode normalization | Platform text APIs | quartz/.../UnicodeNormalizer.kt |
| 16 | GZip | object | GZip compression | Platform compression APIs | quartz/.../GZip.kt |
| 17 | LibSodiumInstance | object | NaCl/libsodium (NIP-44 encryption) | Different libs per platform | quartz/.../LibSodiumInstance.kt |
| 18 | Log | object | Logging | Platform logging systems | quartz/.../Log.kt |
| 19 | BigDecimal | class | Arbitrary precision decimal | Not in Kotlin common stdlib | quartz/.../BigDecimal.kt |
| 20 | BitSet | class | Bit set data structure | Not in Kotlin common stdlib | quartz/.../BitSet.kt |
| 21 | ServerInfoParser | object | Server info parsing (NIP-96) | Platform JSON parsing | quartz/.../nip96.../ServerInfoParser.kt |
| 22 | EventHasherSerializer | object | Event hashing | Platform-specific optimizations | quartz/.../nip01Core.../EventHasherSerializer.kt |
| 23 | OptimizedJsonMapper | object | JSON mapping | Platform JSON libraries | quartz/.../nip01Core.../OptimizedJsonMapper.kt |
| 24 | Address | data class | Address data structure | Platform-specific string handling | quartz/.../nip01Core.../Address.kt |
Pattern Analysis
Objects (Singletons) - 19 total
Most common pattern for platform-specific singletons:
- Crypto: Secp256k1Instance, LibSodiumInstance, Sha256
- I/O: UriParser, UrlEncoder, GZip
- Utils: Log, Platform, SecureRandom
Classes (Instantiable) - 4 total
For objects that need to maintain state:
- AESCBC, AESGCM (cipher state)
- DigestInstance, MacInstance (hash/MAC state)
- BigDecimal, BitSet (data structures)
Functions - 2 total
Simple utilities:
- platform(), currentTimeSeconds()
Why Abstracted Categories
Crypto (8 items)
Always abstract: Security APIs fundamentally different across platforms
- Android: Android Keystore, secp256k1-android
- Desktop: JVM crypto, secp256k1-jvm
- iOS: Security framework, native crypto
I/O & Platform Utils (7 items)
Often abstract: File systems, URLs, compression differ
- Platform storage APIs
- URL handling varies
- Compression libraries differ
Data Structures (2 items)
Abstract when missing: Not available in Kotlin common stdlib
- BigDecimal, BitSet not in common
JSON/Parsing (3 items)
Platform-specific optimization: Uses platform JSON libraries
- Android/Desktop: Jackson (via jvmAndroid)
- iOS: Native parsers
Logging (1 item)
Always abstract: Platform logging systems differ
- Android: android.util.Log
- Desktop: println or logging framework
- iOS: NSLog or OSLog
Actual Implementation Examples
Simple Object Pattern
// commonMain
expect object Log {
fun d(tag: String, message: String)
}
// androidMain
actual object Log {
actual fun d(tag: String, message: String) {
android.util.Log.d(tag, message)
}
}
// jvmMain
actual object Log {
actual fun d(tag: String, message: String) {
println("[$tag] $message")
}
}Complex Object with Dependencies
// commonMain
expect object Secp256k1Instance {
fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray
}
// androidMain - uses JNI bindings
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
}
}
// jvmMain - different JNI library
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.signSchnorr(data, privKey, null)
}
}
// iosMain - native iOS implementation
actual object Secp256k1Instance {
actual fun signSchnorr(data: ByteArray, privKey: ByteArray): ByteArray {
// Uses iOS Security framework or native lib
}
}Class Pattern
// commonMain
expect class BigDecimal {
constructor(value: String)
fun add(other: BigDecimal): BigDecimal
override fun toString(): String
}
// jvmAndroid (works on Android + Desktop)
actual typealias BigDecimal = java.math.BigDecimal
// iosMain
actual class BigDecimal {
private val value: NSDecimalNumber
actual constructor(value: String) {
this.value = NSDecimalNumber(value)
}
// ... implementation
}Decision Patterns
Ask for each declaration: 1. Used by 2+ platforms? → YES (otherwise platform-specific) 2. Pure Kotlin possible? → NO (otherwise commonMain) 3. Varies by platform? → YES (expect/actual) 4. JVM-only library? → NO (otherwise jvmAndroid)
Source Set Hierarchy in Amethyst
Visual guide to source set organization with concrete examples from the codebase.
Hierarchy Diagram
┌─────────────────────────────────────────────────────────────┐
│ commonMain │
│ Pure Kotlin, no platform APIs │
│ Examples: │
│ - Nostr event parsing (TextNoteEvent, MetadataEvent) │
│ - Business logic (data validation, crypto algorithms) │
│ - Data models (@Immutable data classes) │
│ Dependencies: kotlin-stdlib, kotlinx-coroutines │
└──────────────────────┬──────────────────────────────────────┘
│
┌────────────┴────────────┬───────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌───────────────────┐ ┌──────────────┐
│ jvmAndroid │ │ iosMain │ │ Future: │
│ JVM libraries │ │ iOS common │ │ jsMain │
│ Examples: │ │ Examples: │ │ wasmMain │
│ - Jackson JSON │ │ - Platform API │ └──────────────┘
│ - OkHttp HTTP │ │ - Actuals for │
│ - url-detector │ │ crypto/I/O │
│ Dependencies: │ │ Dependencies: │
│ - Jackson │ │ - Platform libs │
│ - OkHttp │ └───────┬───────────┘
└────┬─────────┬───┘ │
│ │ │
│ │ ├─→ iosArm64Main (device ARM64)
│ │ └─→ iosSimulatorArm64Main (Apple Silicon)
▼ ▼
┌──────────┐ ┌───────────┐
│android │ │ jvmMain │
│Main │ │ (Desktop) │
│Examples: │ │ Examples: │
│- Activity│ │- Window │
│- ViewModel│ │- MenuBar │
│- Android │ │- Desktop │
│ APIs │ │ Compose │
│Deps: │ │ Deps: │
│- secp256k│ │- secp256k │
│ 1-android│ │ 1-jvm │
│- androidx│ │- Compose │
│ │ │ Desktop │
└──────────┘ └───────────┘Dependency Flow
Code in commonMain
↓ can use
Nothing (only Kotlin stdlib)
Code in jvmAndroid
↓ can use
commonMain + JVM libraries (Jackson, OkHttp)
Code in androidMain
↓ can use
commonMain + jvmAndroid + Android framework
Code in jvmMain
↓ can use
commonMain + jvmAndroid + JVM + Compose Desktop
Code in iosMain
↓ can use
commonMain + iOS platform APIsReal Examples from Amethyst
commonMain - Pure Kotlin
File: quartz/src/commonMain/.../TextNoteEvent.kt
@Immutable
class TextNoteEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : BaseThreadedEvent(...) {
// Pure Kotlin - works everywhere
override fun indexableContent() = "Subject: " + subject() + "\n" + content
}Why commonMain:
- Pure Kotlin code
- No platform APIs
- Data class with business logic
- Needed by all platforms
---
jvmAndroid - JVM Libraries
File: quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
dependsOn(commonMain.get())
dependencies {
// Normalizes URLs
api(libs.rfc3986.normalizer)
// Performant Parser of JSONs into Events
api(libs.jackson.module.kotlin)
// Parses URLs from Text
api(libs.url.detector)
// Websockets API
implementation(libs.okhttp)
implementation(libs.okhttpCoroutines)
}
}
jvmMain { dependsOn(jvmAndroid) } // Desktop gets Jackson, OkHttp
androidMain { dependsOn(jvmAndroid) } // Android gets Jackson, OkHttpWhy jvmAndroid:
- Jackson, OkHttp are JVM-only libraries
- Works on Android (JVM) and Desktop (JVM)
- Does NOT work on iOS (not JVM) or web (not JVM)
Usage in code:
// Can use Jackson in jvmAndroid source set
val mapper = ObjectMapper()
val event = mapper.readValue(json, Event::class.java)---
androidMain - Android Platform
File: amethyst/src/main/.../MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() // Android API
super.onCreate(savedInstanceState)
setContent { // Compose for Android
AmethystTheme {
val accountStateViewModel: AccountStateViewModel = viewModel()
AccountScreen(accountStateViewModel)
}
}
}
}Why androidMain:
- AppCompatActivity is Android framework
- Activity lifecycle Android-specific
- AndroidX libraries (viewModel())
Dependencies:
androidMain {
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
dependencies {
implementation(libs.androidx.core.ktx)
api(libs.secp256k1.kmp.jni.android) // Android crypto
}
}---
jvmMain - Desktop Platform
File: desktopApp/src/jvmMain/.../Main.kt
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp
)
Window( // Compose Desktop API
onCloseRequest = ::exitApplication,
state = windowState,
title = "Amethyst"
) {
MenuBar { // Desktop-specific
Menu("File") {
Item("New Note", shortcut = KeyShortcut(Key.N, ctrl = true))
}
}
NavigationRail { ... } // Sidebar
}
}Why jvmMain:
- Window, MenuBar, NavigationRail are Compose Desktop
- Keyboard shortcuts desktop paradigm
- Different UX from Android (sidebar vs bottom nav)
Dependencies:
jvmMain {
dependsOn(jvmAndroid) // Gets Jackson, OkHttp
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm) // Desktop crypto
implementation(compose.desktop.currentOs)
}
}---
iosMain - iOS Platform
File: quartz/build.gradle.kts
iosMain {
dependsOn(commonMain.get())
dependencies {
// iOS platform dependencies
}
}
val iosArm64Main by getting { dependsOn(iosMain.get()) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) }Why iosMain:
- iOS platform APIs
- Native crypto (Security framework)
- Different from Android/Desktop
Architecture targets:
- iosArm64Main: Device (iPhone, iPad)
- iosSimulatorArm64Main: Apple Silicon simulator
---
Build Order Matters
CRITICAL: jvmAndroid must be defined BEFORE androidMain and jvmMain:
// ✅ CORRECT ORDER
val jvmAndroid = create("jvmAndroid") { ... }
jvmMain { dependsOn(jvmAndroid) }
androidMain { dependsOn(jvmAndroid) }
// ❌ WRONG - Build error
androidMain { dependsOn(jvmAndroid) } // jvmAndroid not defined yet!
val jvmAndroid = create("jvmAndroid") { ... }See comment in quartz/build.gradle.kts:131:
// Must be defined before androidMain and jvmMain
val jvmAndroid = create("jvmAndroid") { ... }Choosing the Right Source Set
Decision flowchart:
Q: Where should this code go?
├─ Pure Kotlin? (no platform APIs)
│ └─ commonMain
│
├─ JVM library? (Jackson, OkHttp)
│ └─ jvmAndroid
│
├─ Android API? (Activity, Context)
│ └─ androidMain
│
├─ Desktop API? (Window, MenuBar)
│ └─ jvmMain
│
└─ iOS API? (platform.posix, Security)
└─ iosMainFuture: Web/wasm Source Sets
Not yet implemented, but structure would be:
commonMain
├─→ jsMain (JavaScript/Web)
│ └─ JS-specific: DOM APIs, fetch
│
└─→ wasmMain (WebAssembly)
└─ wasm-specific: limited APIsConstraints:
- Cannot use jvmAndroid (Jackson, OkHttp)
- Cannot use platform.posix
- Must use pure Kotlin or web-compatible libs (ktor, kotlinx.serialization)
Summary Table
| Source Set | Extends | Can Use | Example Code |
|---|---|---|---|
| commonMain | - | Kotlin stdlib only | TextNoteEvent, business logic |
| jvmAndroid | commonMain | JVM libs (Jackson, OkHttp) | JSON parsing, HTTP |
| androidMain | jvmAndroid | Android framework | Activity, ViewModel |
| jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar |
| iosMain | commonMain | iOS platform | Security framework |
| iosMain | Simulator (Intel) | Architecture-specific | |
| iosArm64Main | iosMain | Device (ARM64) | Architecture-specific |
| jsMain | commonMain | JS/DOM | Web (future) |
| wasmMain | commonMain | wasm APIs | WebAssembly (future) |
Target Compatibility Guide
Current targets (Android, JVM/Desktop, iOS) and future targets (web, wasm) with constraints.
Current Primary Targets
Android (androidMain)
Status: ✅ Mature, production-ready
Runtime: JVM (Dalvik/ART)
Available:
- Android framework (Activity, Context, Intent, etc.)
- AndroidX libraries (ViewModel, Navigation, etc.)
- JVM libraries via jvmAndroid (Jackson, OkHttp)
- Platform-specific crypto:
secp256k1-kmp-jni-android(0.23.0 inlibs.versions.toml)
Constraints:
- Mobile UX paradigms (bottom navigation, vertical scroll)
- Touch-first interaction
- Limited screen space
- Battery/performance constraints
Example code:
// androidMain
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Android-specific lifecycle
}
}---
JVM / Desktop (jvmMain)
Status: ✅ Active development, functional
Runtime: JVM
Available:
- Pure JVM libraries
- JVM libraries via jvmAndroid (Jackson, OkHttp)
- Compose Desktop (Window, MenuBar, etc.)
- Platform-specific crypto:
secp256k1-kmp-jni-jvm(same 0.23.0 line)
Constraints:
- Desktop UX paradigms (sidebar, menus, keyboard shortcuts)
- Keyboard + mouse interaction
- Larger screen space
- Different navigation patterns (no back stack)
Example code:
// jvmMain
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Amethyst"
) {
MenuBar { ... } // Desktop-specific
NavigationRail { ... } // Sidebar
}
}---
iOS (iosMain + architecture targets)
Status: ✅ Mature — actively built and tested
Runtime: Native iOS
Source sets:
- iosMain (common iOS code)
- iosArm64Main (device - iPhone/iPad)
- iosSimulatorArm64Main (Apple Silicon simulator)
- iosX64Main (Intel simulator)
- macosArm64Main (host tooling / XCFramework build)
Available:
- iOS platform APIs (platform.posix, Foundation, etc.)
- Native crypto (Security framework)
- SwiftUI integration (via KMP framework)
NOT available:
- JVM libraries (Jackson, OkHttp)
- jvmAndroid source set
- JVM-specific APIs
Constraints:
- Mobile UX (similar to Android)
- Swift/Objective-C interop
- XCFramework distribution
- CocoaPods integration
Example code:
// iosMain
actual object Secp256k1Instance {
actual fun signSchnorr(...): ByteArray {
// Use iOS Security framework
}
}XCFramework setup:
// quartz/build.gradle.kts
kotlin {
listOf(macosArm64(), iosArm64(), iosSimulatorArm64())
.forEach { target ->
target.binaries.framework {
baseName = "quartz-kmpKit"
isStatic = true
}
}
}---
Future Targets
Web / JavaScript (jsMain)
Status: ❌ Not implemented, consider for future
Runtime: JavaScript (browser or Node.js)
Available:
- Kotlin/JS stdlib
- JS/DOM APIs
- kotlinx.* libraries (serialization, coroutines, datetime)
- ktor-client (HTTP)
NOT available:
- ❌ JVM libraries (Jackson, OkHttp)
- ❌ jvmAndroid source set
- ❌ platform.posix (no file system access like native)
- ❌ Blocking APIs (different async model - JS event loop)
Constraints:
- Single-threaded event loop
- No blocking calls
- Different async patterns (Promises, async/await)
- Browser security (CORS, no file system)
Migration path from current code:
| Current (jvmAndroid) | Web-compatible alternative |
|---|---|
| Jackson JSON | kotlinx.serialization |
| OkHttp HTTP | ktor-client |
| java.math.BigDecimal | Kotlin BigDecimal (coming) |
| Blocking I/O | Suspending functions |
Example migration:
// Current: jvmAndroid
val mapper = ObjectMapper()
val event = mapper.readValue(json, Event::class.java)
// Future: commonMain (works on web)
val json = Json { ignoreUnknownKeys = true }
val event = json.decodeFromString<Event>(jsonString)---
WebAssembly (wasmMain)
Status: ❌ Not implemented, experimental Kotlin/Wasm
Runtime: WebAssembly
Available:
- Kotlin/Wasm stdlib
- Limited kotlinx.* libraries
- wasm-specific APIs
NOT available:
- ❌ JVM libraries
- ❌ Full platform.posix
- ❌ Many kotlinx libraries (limited wasm support)
Constraints:
- Even more limited than JS
- Experimental Kotlin support
- Limited library ecosystem
Recommendation: Focus on web (jsMain) first, wasm later.
---
Cross-Target Compatibility Matrix
| Feature | Android | JVM/Desktop | iOS | Web (JS) | wasm |
|---|---|---|---|---|---|
| Pure Kotlin | ✅ | ✅ | ✅ | ✅ | ✅ |
| kotlinx.coroutines | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| kotlinx.serialization | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| kotlinx.datetime | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| ktor-client | ✅ | ✅ | ✅ | ✅ | ❌ |
| Jackson JSON | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
| OkHttp | ✅ (jvmAndroid) | ✅ (jvmAndroid) | ❌ | ❌ | ❌ |
| platform.posix | ❌ | ❌ | ✅ | ❌ | ⚠️ |
| Compose Multiplatform | ✅ | ✅ | ⚠️ (experimental) | ⚠️ (experimental) | ❌ |
Legend:
- ✅ Full support
- ⚠️ Limited/experimental
- ❌ Not available
---
Future-Proofing Recommendations
For Web Compatibility
DO:
- ✅ Use kotlinx.serialization instead of Jackson
- ✅ Use ktor-client instead of OkHttp
- ✅ Use kotlinx.datetime instead of java.time
- ✅ Use suspending functions (non-blocking)
- ✅ Keep business logic in commonMain
DON'T:
- ❌ Put JVM libraries in commonMain
- ❌ Use platform.posix for critical features
- ❌ Use blocking I/O
- ❌ Depend on threading (use coroutines)
Example:
// ❌ NOT web-compatible
// jvmAndroid
fun parseJson(json: String): Event {
val mapper = ObjectMapper() // Jackson - JVM only
return mapper.readValue(json, Event::class.java)
}
// ✅ Web-compatible
// commonMain
@Serializable
data class Event(...)
fun parseJson(json: String): Event {
return Json.decodeFromString<Event>(json) // Works everywhere
}Current Migration Priorities
High priority: (Needed for web) 1. Migrate Jackson → kotlinx.serialization 2. Migrate OkHttp → ktor-client 3. Move business logic to commonMain
Medium priority: (Nice to have) 1. Abstract date/time handling → kotlinx.datetime 2. Remove platform.posix usage where possible 3. Use suspending functions over blocking
Low priority: (Future optimization) 1. wasm-specific optimizations 2. Platform-specific performance tuning
---
Platform-Specific Patterns
Android vs iOS Differences
| Aspect | Android | iOS |
|---|---|---|
| Activity/ViewController | Activity | UIViewController |
| Navigation | Compose Navigation | UINavigationController |
| Lifecycle | onCreate, onResume, etc. | viewDidLoad, viewWillAppear |
| Permissions | Runtime permissions | Info.plist + runtime |
| Crypto | secp256k1-android | Security framework |
| Storage | Room, SharedPreferences | Core Data, UserDefaults |
Desktop vs Mobile Differences
| Aspect | Desktop | Mobile |
|---|---|---|
| Navigation | Sidebar | Bottom nav |
| Input | Keyboard + mouse | Touch |
| Screen | Large, landscape | Small, portrait |
| Windows | Multi-window | Single app |
| Shortcuts | Keyboard shortcuts (Ctrl+N) | None |
| Menus | MenuBar | Bottom sheets |
---
Testing Strategy
Per-Target Testing
Android:
- Unit tests: androidTest
- Instrumented: androidInstrumentedTest
- Device/emulator testing
Desktop:
- Unit tests: jvmTest
- Manual desktop app testing
iOS:
- Unit tests: iosTest (iosArm64Test, etc.)
- Simulator/device testing
Web (future):
- Unit tests: jsTest
- Browser testing (Selenium, Playwright)
Shared Testing
commonTest:
- Business logic tests
- Pure Kotlin code
- Works on all platforms
// commonTest
class EventParsingTest {
@Test
fun parseTextNoteEvent() {
// Tests run on all platforms
}
}---
Summary
Current Focus: Android, JVM/Desktop, iOS (active development)
Future Considerations: Web (requires migration from Jackson/OkHttp)
Key Decision: Prefer kotlinx.* libraries over JVM-specific libs for future web compatibility.
#!/bin/bash
# Suggests KMP library alternatives for JVM-specific dependencies
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "=== KMP Dependency Suggestions ==="
echo
# Colors
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
SUGGESTIONS_FOUND=0
# Check for Jackson (suggest kotlinx.serialization)
echo "📦 Checking for Jackson JSON..."
if grep -r "jackson" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
echo -e "${YELLOW}⚠ Found Jackson dependency${NC}"
echo " Current: Jackson (JVM-only)"
echo -e " ${GREEN}Suggest: kotlinx.serialization${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Remove:"
echo " api(libs.jackson.module.kotlin)"
echo
echo " // Add to commonMain:"
echo " implementation(libs.kotlinx.serialization.json)"
echo
echo " // Code change:"
echo " // Before (Jackson):"
echo " val mapper = ObjectMapper()"
echo " val event = mapper.readValue(json, Event::class.java)"
echo
echo " // After (kotlinx.serialization):"
echo " @Serializable"
echo " data class Event(...)"
echo " val event = Json.decodeFromString<Event>(json)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using Jackson (or already using kotlinx.serialization)${NC}"
fi
# Check for OkHttp (suggest ktor)
echo
echo "📦 Checking for OkHttp..."
if grep -r "okhttp" */build.gradle.kts 2>/dev/null | grep -q "implementation\|api"; then
echo -e "${YELLOW}⚠ Found OkHttp dependency${NC}"
echo " Current: OkHttp (JVM-only)"
echo -e " ${GREEN}Suggest: ktor-client${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Remove:"
echo " implementation(libs.okhttp)"
echo
echo " // Add to commonMain:"
echo " implementation(libs.ktor.client.core)"
echo " // Platform-specific engines:"
echo " // androidMain: implementation(libs.ktor.client.android)"
echo " // jvmMain: implementation(libs.ktor.client.cio)"
echo " // iosMain: implementation(libs.ktor.client.darwin)"
echo
echo " // Code change:"
echo " // Before (OkHttp):"
echo " val client = OkHttpClient()"
echo " val request = Request.Builder().url(url).build()"
echo " val response = client.newCall(request).execute()"
echo
echo " // After (ktor):"
echo " val client = HttpClient()"
echo " val response: String = client.get(url)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using OkHttp (or already using ktor)${NC}"
fi
# Check for java.time (suggest kotlinx.datetime)
echo
echo "📦 Checking for java.time usage..."
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.time\." >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found java.time imports${NC}"
echo " Current: java.time (JVM-only)"
echo -e " ${GREEN}Suggest: kotlinx.datetime${NC} (works on all platforms)"
echo
echo " Migration:"
echo " // Add to commonMain:"
echo " implementation(libs.kotlinx.datetime)"
echo
echo " // Code change:"
echo " // Before (java.time):"
echo " import java.time.Instant"
echo " val now = Instant.now()"
echo
echo " // After (kotlinx.datetime):"
echo " import kotlinx.datetime.Clock"
echo " val now = Clock.System.now()"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using java.time (or already using kotlinx.datetime)${NC}"
fi
# Check for java.math.BigDecimal
echo
echo "📦 Checking for java.math.BigDecimal usage..."
if find */src -name "*.kt" 2>/dev/null | xargs grep -l "import java.math.BigDecimal" >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found java.math.BigDecimal imports${NC}"
echo " Current: java.math.BigDecimal (JVM-only)"
echo -e " ${BLUE}Note:${NC} KMP BigDecimal not yet in stable kotlinx"
echo
echo " Options:"
echo " 1. Use expect/actual (current approach in quartz)"
echo " 2. Wait for kotlinx.decimal (proposal stage)"
echo " 3. Use third-party KMP library (e.g., bignum)"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using java.math.BigDecimal directly${NC}"
fi
# Check for platform.posix usage
echo
echo "📦 Checking for platform.posix usage..."
if find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "import platform.posix\." >/dev/null 2>&1; then
echo -e "${YELLOW}⚠ Found platform.posix in commonMain${NC}"
echo " Current: platform.posix (native platforms only, not web)"
echo -e " ${GREEN}Suggest:${NC} Abstract file I/O with expect/actual"
echo
echo " For web compatibility:"
echo " - iOS/Native: platform.posix"
echo " - Web: Use kotlinx-io or ktor file APIs"
echo " - Create expect/actual for file operations"
echo
SUGGESTIONS_FOUND=$((SUGGESTIONS_FOUND + 1))
else
echo -e "${GREEN}✓ Not using platform.posix in commonMain${NC}"
fi
# Summary
echo
echo "=== Summary ==="
if [ "$SUGGESTIONS_FOUND" -eq 0 ]; then
echo -e "${GREEN}✓ No JVM-specific dependencies found!${NC}"
echo " Your code is ready for web/wasm targets."
else
echo -e "${YELLOW}Found $SUGGESTIONS_FOUND suggestion(s) for KMP alternatives${NC}"
echo
echo "Priority recommendations:"
echo " 1. ${GREEN}High:${NC} Jackson → kotlinx.serialization (enables web support)"
echo " 2. ${GREEN}High:${NC} OkHttp → ktor-client (enables web support)"
echo " 3. ${GREEN}Medium:${NC} java.time → kotlinx.datetime"
echo " 4. ${GREEN}Low:${NC} Consider web compatibility for platform.posix usage"
echo
echo "Resources:"
echo " - kotlinx.serialization: https://github.com/Kotlin/kotlinx.serialization"
echo " - ktor: https://ktor.io/docs/client.html"
echo " - kotlinx.datetime: https://github.com/Kotlin/kotlinx-datetime"
fi
exit 0
#!/bin/bash
# Validates KMP source set structure and detects common issues
set -e
PROJECT_ROOT="${1:-.}"
cd "$PROJECT_ROOT"
echo "=== Validating KMP Structure ==="
echo
# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
ISSUES_FOUND=0
# Check 1: jvmAndroid defined before androidMain/jvmMain
echo "📋 Checking source set definition order..."
if [ -f "quartz/build.gradle.kts" ]; then
jvmandroid_line=$(grep -n "val jvmAndroid = create" quartz/build.gradle.kts | cut -d: -f1)
android_line=$(grep -n "androidMain {" quartz/build.gradle.kts | cut -d: -f1)
jvm_line=$(grep -n "jvmMain {" quartz/build.gradle.kts | cut -d: -f1)
if [ -n "$jvmandroid_line" ] && [ -n "$android_line" ] && [ -n "$jvm_line" ]; then
if [ "$jvmandroid_line" -lt "$android_line" ] && [ "$jvmandroid_line" -lt "$jvm_line" ]; then
echo -e "${GREEN}✓${NC} jvmAndroid defined before androidMain and jvmMain"
else
echo -e "${RED}✗${NC} jvmAndroid must be defined BEFORE androidMain and jvmMain"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
fi
fi
fi
# Check 2: Platform code in commonMain (Android imports)
echo
echo "📋 Checking for platform code in commonMain..."
android_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import android\." || true)
if [ -n "$android_imports_in_common" ]; then
echo -e "${RED}✗${NC} Found Android imports in commonMain:"
echo "$android_imports_in_common" | sed 's/^/ /'
echo " Fix: Move to androidMain or create expect/actual"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
else
echo -e "${GREEN}✓${NC} No Android imports in commonMain"
fi
# Check 3: JVM libraries in commonMain (Jackson, OkHttp)
echo
echo "📋 Checking for JVM libraries in commonMain..."
jvm_imports_in_common=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^import com.fasterxml.jackson\|^import okhttp3\." || true)
if [ -n "$jvm_imports_in_common" ]; then
echo -e "${RED}✗${NC} Found JVM library imports in commonMain:"
echo "$jvm_imports_in_common" | sed 's/^/ /'
echo " Fix: Move to jvmAndroid or migrate to kotlinx.serialization/ktor"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
else
echo -e "${GREEN}✓${NC} No JVM library imports in commonMain"
fi
# Check 4: Unmatched expect/actual declarations
echo
echo "📋 Checking expect/actual pairs..."
expect_files=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -l "^expect " || true)
if [ -n "$expect_files" ]; then
for file in $expect_files; do
# Extract declarations
expects=$(grep "^expect \(class\|object\|fun\|interface\)" "$file" | sed 's/expect //' | awk '{print $2}' | sed 's/[({].*$//')
# Check for actuals in platform source sets
for expect_name in $expects; do
actual_count=0
for platform in androidMain jvmMain iosMain; do
platform_dir=$(dirname "$file" | sed "s/commonMain/$platform/")
platform_file="${platform_dir}/$(basename "$file")"
if [ -f "$platform_file" ] && grep -q "actual.*$expect_name" "$platform_file"; then
actual_count=$((actual_count + 1))
fi
done
if [ "$actual_count" -eq 0 ]; then
echo -e "${YELLOW}⚠${NC} No actual implementations found for: $expect_name in $file"
echo " Check: androidMain, jvmMain, iosMain"
ISSUES_FOUND=$((ISSUES_FOUND + 1))
fi
done
done
else
echo -e "${GREEN}✓${NC} No expect declarations to validate"
fi
# Check 5: Duplicated business logic across platforms
echo
echo "📋 Checking for potential code duplication..."
# This is a heuristic check - look for similar function names in different platform source sets
common_functions=$(find */src/commonMain -name "*.kt" 2>/dev/null | xargs grep -h "^fun " | awk '{print $2}' | sed 's/[({<].*$//' | sort -u || true)
if [ -n "$common_functions" ]; then
for func in $common_functions; do
android_count=$(find */src/androidMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
jvm_count=$(find */src/jvmMain -name "*.kt" 2>/dev/null | xargs grep -l "^fun $func" | wc -l)
if [ "$android_count" -gt 0 ] && [ "$jvm_count" -gt 0 ]; then
echo -e "${YELLOW}⚠${NC} Function '$func' found in both androidMain and jvmMain"
echo " Consider: Move to commonMain or jvmAndroid if truly shared"
fi
done
fi
# Summary
echo
echo "=== Summary ==="
if [ "$ISSUES_FOUND" -eq 0 ]; then
echo -e "${GREEN}✓ All checks passed!${NC}"
exit 0
else
echo -e "${RED}✗ Found $ISSUES_FOUND issue(s)${NC}"
echo
echo "Common fixes:"
echo " 1. Platform code in commonMain → Move to androidMain or create expect/actual"
echo " 2. JVM libraries in commonMain → Move to jvmAndroid or migrate to kotlinx.*"
echo " 3. Missing actual implementations → Implement in all target platforms"
echo " 4. Duplicated logic → Move to commonMain or jvmAndroid"
exit 1
fi
Related skills
How it compares
Pick kotlin-multiplatform for production expect/actual case studies; pick generic KMP tutorials when you need beginner syntax without real codebase rationales.
FAQ
When does code belong in commonMain?
When used by two or more platforms and written in pure Kotlin without platform APIs.
What is jvmAndroid for?
Shared JVM code such as Jackson parsing used by Android and desktop but not iOS.
When should UI stay platform-specific?
When navigation or UI frameworks differ too much, such as Activity versus Window patterns.
Is Kotlin Multiplatform safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.