---
name: kotlin-multiplatform
description: KMP placement and Gradle in Amethyst: which module or source set, expect/actual vs ports, verifyKmpPurity, extracting Android composables to commonsUI, build.gradle.kts/libs.versions.toml edits, secp256k1 JNI variants, R8.
---

# Kotlin Multiplatform and Gradle in Amethyst

Module roles, the "share vs keep native" rule and the CLI-safe boundary are in
`AGENTS.md` (Architecture, Feature Workflow). Package placement inside `commons` /
`commonsUI` is in `commons/ARCHITECTURE.md` ("Where does my code go?", "Source sets")
and `commonsUI/ARCHITECTURE.md` ("Platform slots"). Read those first; this skill
adds the source-set mechanics and the build traps.

## Modules

`settings.gradle.kts` includes 13: `amethyst`, `nappletHost`, `benchmark`,
`baselineprofile`, `geode`, `commons`, `commonsUI`, `nestsClient`,
`marmotQuic`, `desktopApp`, `cli`, `relayBench`, `marmotBench`. Quartz is the
external `libs.quartz` (vitorpamplona/quartz) and QUIC the external `libs.quic`
(vitorpamplona/quic-kmp); `-PquartzDir=../quartz` / `-PquicKmpDir=../quic-kmp` build a
local checkout instead (an included build, so quic's own quartz dependency follows).

## Source sets

| Source set | Holds | Targets |
|---|---|---|
| `commonMain` | Pure Kotlin. Compiled for iOS/native too, so no JVM APIs | all |
| `jvmAndroid` | JVM libraries shared by Android + Desktop: OkHttp, `nestsClient`, NWC/LNURL | android, jvm |
| `androidMain` / `jvmMain` | Platform actuals (Keystore/DataStore vs keyring, Coil bridges) | one each |
| `iosMain`, `appleMain`, `nativeMain`, `linuxMain`, `macosMain` | quartz only (plus `iosMain` in commons/commonsUI) | Apple / native |
| `skikoMain` | commonsUI code shared by Desktop and iOS | jvm, ios |

Targets: `quartz` builds android, jvm, iosArm64, iosSimulatorArm64, linuxX64,
macosArm64. `commons` and `commonsUI` build android, jvm, iosArm64,
iosSimulatorArm64. `nestsClient`, `marmotQuic` are android + jvm. There is
no `iosX64`.

`jvmAndroid` is a custom intermediate set created with `create("jvmAndroid")`
before `androidMain`/`jvmMain` depend on it (see `commons/build.gradle.kts`).
Prefer the most common set that compiles: `commonMain` → `jvmAndroid` → platform.

kotlinx.serialization is the house JSON stack and OkHttp the house HTTP stack
(`AGENTS.md` › Tech Stack). Jackson is left only inside quartz's JVM code and reaches
the app modules transitively; don't add new Jackson use here. Don't migrate JVM HTTP
to Ktor. Jackson and OkHttp are banned in iOS-compiled sets, which the purity gate
enforces.

## expect/actual vs ports and slots

- **expect/actual** for a small platform verb or value with no app state:
  `KmpLock`, `ConcurrentSet`, `SecureKeyStorage`, `isDebug`, `formattedDateTime`
  (commons), `rememberTextSharer`, `rememberShortNotice` (commonsUI). Put the actual
  in `jvmAndroid` when Android and Desktop share it.
- **A port** (interface the shell implements) when the platform side needs app
  state or services: `AccountViewModelHost`, `UserNoticeSink`, `LocalCacheHost`.
- **A slot** (CompositionLocal the front end installs) for things only a front end
  can draw: `LocalNotePlatform`, `LocalRichTextPlatform`.
- Not every quartz singleton is expect: `Secp256k1Instance`, `Log` and
  `LibSodiumInstance` are plain `object`s in `quartz/.../utils/`.
- Don't `expect` a whole screen or navigation; share it and put only the leaf
  behind a port or slot.

## Purity gate: `verifyKmpPurity`

`gradle/kmp-purity.gradle.kts`, applied by `commons` and `commonsUI` (quartz keeps a copy) and
wired into `check`, scans every iOS/native-compiled source set (`commonMain`,
`iosMain`, `skikoMain`, …, and their tests) and fails on `com.fasterxml.jackson`,
`okhttp3`, `System.currentTimeMillis` (use `TimeUtils.now()`), `Thread.sleep`,
`java.util.UUID` (use `kotlin.uuid.Uuid`), `kotlin.jvm.Synchronized` or bare
`synchronized(` (use `KmpLock`), `kotlin.jvm.Volatile` (use
`kotlin.concurrent.Volatile`). CI runs it on Linux in the lint job; the macOS
`test-commons-ios` / `test-commonsui-ios` jobs are authoritative. Run
`./gradlew :commons:verifyKmpPurity :commonsUI:verifyKmpPurity`
after touching `commonMain`. Quartz has its own copy of the task and its own iOS CI.

## Gradle traps

- **Versions live only in `gradle/libs.versions.toml`.** Add the library there and
  reference `libs.*`; never hardcode a version or copy one into docs. Check the
  license first (`AGENTS.md` › Dependency Licensing).
- **`test` is an alias for each KMP module's `jvmTest`.** Other targets
  (`:commonsUI:testAndroidHostTest`, the native ones) run only when named; see
  `AGENTS.md` › Build Commands.
- **testFixtures in KMP:** the source-set `dependencies { }` block
  (`KotlinDependencyHandler`) has no `testFixtures(...)`. Use a top-level block with
  the configuration name as a string:
  `dependencies { "jvmAndroidTestImplementation"(testFixtures(project(":geode"))) }`.
- **secp256k1 variants:** `secp256k1-kmp-common` in `commonMain`,
  `secp256k1-kmp-jni-android` in `androidMain`, `secp256k1-kmp-jni-jvm` in `jvmMain`
  and JVM tests. All three share one `version.ref`. A JVM classpath with the
  android variant fails at load with `UnsatisfiedLinkError`.
- **R8:** library keep rules ship in `quartz/consumer-rules.pro`. Never add a blanket
  `-keep class com.vitorpamplona.quartz.** { *; }`. R8 can't see reflection, so a
  moved/renamed reflective class is a silent release break: before tagging, run
  `python3 tools/r8-verify/verify_reflection_contract.py amethyst/build/outputs/mapping/<variant>/`
  (the release workflow checks `playRelease` and `fdroidRelease`) and update
  `tools/r8-verify/reflection-contract.txt` when you move a listed class.

## Extracting Android code to shared modules

Steps and the Android → multiplatform replacement table:
[references/extract-composable.md](references/extract-composable.md).


---
Source attribution: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/kotlin-multiplatform/SKILL.md
License: MIT
License information: https://opensource.org/license/mit
Original upstream instructions; downloaded from Mega AI Hub.

## Upstream license notice

MIT License

Copyright (c) 2023 Vitor Pamplona

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



# Mega AI Hub resource metadata

Creator / publisher: vitorpamplona
Original project: https://github.com/vitorpamplona/amethyst/blob/3cbe201baa37fb15cd81cfff6800ab97c7b4a1f9/.claude/skills/kotlin-multiplatform/SKILL.md
Attribution basis: Source repository record

Fields of work: Development & Engineering, Product Management
Roles: Backend Developer, Full-Stack Developer, Mobile Developer, Product Manager, Technical Product Manager
Specialties: Backend, Mobile
Tools: Amethyst
Country applicability: Country not specified
Classification basis: Source catalog labels
Instruction SHA-256: 3d33ae652c502916bfd9578fd6a3a05aa18b96c89028a48ab8aefd682422a3fd
