kotlin-multiplatform について
kotlin-multiplatformは、AmethystのKotlin Multiplatformアーキテクチャを検討するためのガイダンススキルです。共有ソースセットに含めるコードとプラットフォーム固有に保つコードの判断を、コードベースの例や現行・将来のターゲット向けの指針とともに支援します。
主な機能
- 01共有コードとプラットフォーム固有コードの判断
- 02ソースセット依存関係の指針
- 03expect/actualの使用パターン
- 04JVMとターゲット互換性の指針
- 05抽象化に関する一般的な落とし穴
活用シーン
- 01新しいコードのソースセット選択
- 02commonMain内のプラットフォームAPI確認
- 03Android、Desktop、iOS間のコード共有計画
- 04将来のwebやwasmターゲット向け抽象化の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vitorpamplona/ amethyst --skill kotlin-multiplatformインストール後、Claude Code で/kotlin-multiplatformを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/abstraction-examples.mdexpect-actual-catalog.mdsource-set-hierarchy.mdtarget-compatibility.mdscripts/suggest-kmp-dependency.shvalidate-kmp-structure.sh詳細
Kotlin Multiplatform:プラットフォーム抽象化の判断
Amethyst の KMP アーキテクチャに関する専門的なガイダンスです。共有するものとプラットフォーム固有にするものを判断します。
このスキルを使う場面
プラットフォーム抽象化について判断するとき:
- 「expect/actual を作るべきですか、それとも Android 専用のままにすべきですか?」
- 「この ViewModel のロジックを共有できますか?」
- 「この暗号化/JSON/ネットワークの実装は、どこに配置すべきですか?」
- 「これは Android Context を使っていますが、抽象化できますか?」
- 「このコードは間違ったモジュールにありますか?」
- iOS/web/wasm のターゲットに備えるとき
- 不適切な配置を検出するとき
抽象化の判断フローチャート
中心となる問い:「このコードはプラットフォーム間で再利用すべきですか?」
次の手順に沿って判断してください(1 分以内):
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
→ Share it in commonsUI; expect/actual or a slot only for the platform leaf
Example: Navigation. One shell in commonsUI, picked by window size
(ScreenLayoutSpec); only the Activity / Window entry point is native
Final check:
Q: Maintenance cost of abstraction < duplication cost?
├─ YES → Proceed with abstraction
└─ NO → Duplicate (simpler)
コードベースの実例
暗号化 → 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
**理由:**プラットフォームごとにセキュリティ API が異なります。
JSON パース → jvmAndroid:
// quartz/build.gradle.kts
val jvmAndroid = create("jvmAndroid") {
api(libs.jackson.module.kotlin)
}
**理由:**Jackson は JVM 専用で、Android と Desktop では動作しますが、iOS/web では動作しません。
ナビゲーション → 共通、エントリーポイント → プラットフォーム固有:
- 共通(
commonsUIに移行中):ナビゲーションホスト、ボトムバー/レール/ 常時表示ドロワー。ScreenLayoutSpecを使い、ウィンドウサイズに応じて選択します - Android:
MainActivityがホストします。Desktop:Window(+メニューバー、トレイ)がホストします **理由:**Android はノート PC でも動作し、新しい Desktop アプリでも同じ UI を表示します。commons/plans/2026-09-27-one-ui-android-desktop.mdを参照してください。現在のdesktopAppのサイドバーシェルはレガシーです。
考え方:依存関係グラフとしてのソースセット
ソースセットはフォルダーではなく、依存関係グラフとして考えてください。
┌─────────────────────────────────────────────┐
│ 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
**重要な点:**jvmAndroid はプラットフォームではなく、共有 JVM レイヤーです。
jvmAndroid パターン
**Amethyst 独自のパターンです。**Android と Desktop 間で JVM ライブラリを共有します。
jvmAndroid を使う場面
次の場合は jvmAndroid を使います:
- ✅ JVM 固有のライブラリ(Jackson、OkHttp、url-detector)
- ✅ Android の実装と Desktop の実装が同じ場合(同じ JVM)
- ✅ ライブラリが iOS/web で動作しない場合
次の場合は jvmAndroid を使わないでください:
- ❌ 純粋な Kotlin コード(commonMain を使います)
- ❌ プラットフォーム固有の API(androidMain/jvmMain を使います)
- ❌ すべてのプラットフォームで動作すべきコード
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) }
Jackson を commonMain ではなく jvmAndroid に配置する理由:
- Jackson は JVM 固有のライブラリです
- Android で動作します(JVM 上で実行されます)
- Desktop で動作します(JVM 上で実行されます)
- iOS(JVM ではない)や web(JVM ではない)では動作しません
**Web/wasm に関する考慮事項:**将来的な web サポートに備えて、Jackson から kotlinx.serialization への移行を検討してください(「ターゲット固有のガイダンス」を参照)。
抽象化するものとプラットフォーム固有にするもの
コードベースのパターンに基づいた、判断のための簡単なガイドラインです。
常に抽象化する
- 暗号化(Secp256k1、暗号化、署名)
- コアプロトコルのロジック(Nostr イベント、NIP)
- **理由:**あらゆる場所で必要であり、プラットフォームによってセキュリティ API が異なります
多くの場合、抽象化する
- I/O 操作(ファイルの読み取り、キャッシュ)
- ログ記録(プラットフォームによってログシステムが異なります)
- シリアライズ(kotlinx.serialization を使用する場合)
- **理由:**広く再利用され、プラットフォームごとの実装も利用できます
場合によって抽象化する
- **ビジネスロジック:**はい。ステートマシン、データ処理
- **ViewModel:**はい。状態とビジネスロジックは共有できます(StateFlow/SharedFlow)
- **画面レイアウト:**はい。
commonsUIで共有し、プラットフォームではなくウィンドウサイズに合わせて調整します - **理由:**Android(スマートフォンからノート PC まで)と Desktop で 1 つの UI を使い、異なるのはエントリーポイントだけです
あまり抽象化しない
- 複雑な UI コンポーネント(プラットフォームへの依存度が高いコンポーザブル)
- **理由:**プラットフォームごとに設計思想が大きく異なる場合があります
決して抽象化しない
- プロセス/ウィンドウのエントリーポイント(Activity、Service、Window、トレイ)
- 権限(Android と iOS では API に互換性がありません)
- プラットフォーム固有の UX パターン
- **理由:**プラットフォームへの依存度が高すぎるため、抽象化するとリークする API が生じます
shared-ui-analysis.md の根拠
| コンポーネント | 共有? | 根拠 |
|---|---|---|
| PubKeyFormatter、ZapFormatter | ✅ はい | 純粋な Kotlin で、プラットフォーム API を使用しません |
| TimeAgoFormatter | ⚠️ 抽象化 | ローカライズされた文字列に StringProvider が必要です |
| ViewModels(状態+ロジック) | ✅ はい | StateFlow/SharedFlow はプラットフォームに依存せず、Compose Multiplatform のライフサイクルと互換性があります |
| 画面レイアウト(Scaffold、ナビゲーション) | ✅ はい(移行中) | UI は 1 つです。ScreenLayoutSpec により、ウィンドウサイズに応じてボトムバー/レール/ドロワーを選択します |
| 画像の読み込み(Coil) | ⚠️ 抽象化 | Coil 3.x は KMP をサポートしていますが、expect/actual のラッパーが必要です |
expect/actual の仕組み
使用する場合: 2 つ以上のプラットフォームで必要で、プラットフォームごとに異なるコード。
コードベースのパターンカテゴリ
オブジェクト(シングルトン):
// 24 expect declarations found, common pattern:
expect object Secp256k1Instance { ... }
expect object Log { ... }
expect object LibSodiumInstance { ... }
クラス(インスタンス化可能):
expect class AESCBC { ... }
expect class DigestInstance { ... }
関数(ユーティリティ):
expect fun platform(): String
expect fun currentTimeSeconds(): Long
完全なカタログと根拠については、 references/expect-actual-catalog.md を参照してください。
ターゲット別のガイダンス
Android、JVM(デスクトップ)、iOS — 現在の主要ターゲット
ステータス: パターンが成熟しており、API が安定しています
Android(androidMain):
- Android フレームワーク(Activity、Context など)を使用します
- 暗号化には secp256k1-kmp-jni-android(
libs.versions.tomlで0.23.0)を使用します - AndroidX ライブラリ
デスクトップ JVM(jvmMain):
- Compose Desktop(Window、MenuBar など)を使用します
- 暗号化には secp256k1-kmp-jni-jvm(同じ
0.23.0系列)を使用します - 純粋な JVM ライブラリ
iOS(iosMain):
- 成熟したターゲットです。現在も積極的にビルドとテストを行っています
- アーキテクチャのターゲット:iosArm64、iosSimulatorArm64、iosX64(ホストツール用の macosArm64 も含む)
- platform.posix、Security フレームワーク経由でプラットフォーム API を使用します
Web、wasm — 将来のターゲット
ステータス: まだ実装されていません。将来への備えを考慮してください
把握しておくべき制約:
- ❌ platform.posix はありません(ファイル I/O の方式が異なります)
- ❌ JVM ライブラリは使用できません(Jackson、OkHttp は動作しません)
- ❌ 非同期モデルが異なります(スレッドではなく JS イベントループを使用します)
将来への備えのヒント:
- commonMain では純粋な Kotlin を優先します
- kotlinx.* ライブラリを使用します:
- Jackson の代わりに kotlinx.serialization
- OkHttp の代わりに ktor(ktor は Web をサポートします)
- 独自の日付処理の代わりに kotlinx.datetime
- ファイル操作に platform.posix を使用しないでください
- JVM を前提とせずに抽象化が機能することをテストしてください
移行経路の例:
// Current: jvmAndroid (JVM-only)
api(libs.jackson.module.kotlin)
// Future: commonMain (all platforms)
api(libs.kotlinx.serialization.json)
統合:他のスキルを呼び出すタイミング
gradle-expert を呼び出す
次の状況に遭遇した場合は、gradle-expert スキルを呼び出してください:
- 依存関係の競合(例:secp256k1-android と secp256k1-jvm のバージョン不一致)
- ソースセットに関連するビルドエラー
- バージョンカタログの問題(libs.versions.toml)
- 「Duplicate class」エラー
- パフォーマンス/ビルド時間の問題
呼び出し例:
Error: Duplicate class found: fr.acinq.secp256k1.Secp256k1
→ 依存関係の競合解決のため、gradle-expert を呼び出します。
指摘すべき項目
commonMain 内のプラットフォームコード:
// ❌ INCORRECT - Android API in commonMain
expect fun getContext(): Context // Context is Android-only!
→ 指摘:「commonMain 内の Android API は、他のプラットフォームではコンパイルできません」
重複したビジネスロジック:
// ❌ INCORRECT - Same logic in both
// androidMain/.../CryptoUtils.kt
fun validateSignature(...) { ... }
// jvmMain/.../CryptoUtils.kt
fun validateSignature(...) { ... } // Duplicated!
→ 指摘:「ビジネスロジックが重複しています。commonMain に移すか、expect/actual を使用してください」
車輪の再発明を避ける — KMP の代替手段を提案する:
- 独自の日付/時刻処理 → kotlinx.datetime
- OkHttp → ktor(Web をサポート)
- Jackson → kotlinx.serialization
- 独自の UUID → kotlinx.uuid(安定したら)
よくある落とし穴
1. 過剰な抽象化
問題: UI コンポーネントに expect/actual を作成する
// ❌ BAD
expect fun NavigationComponent(...)
理由: プラットフォームごとにナビゲーション UI 全体が分岐します。
修正方法: ナビゲーションは commonsUI に 1 回だけ実装し、プラットフォーム固有の末端部分(Activity の結果やファイルピッカー)のみを expect/actual またはスロットの背後に置きます
2. 共有不足
問題: プラットフォーム間でビジネスロジックを重複させる
// ❌ BAD - duplicated in androidMain and jvmMain
fun parseNostrEvent(json: String): Event { ... }
理由: バグ修正を 2 回適用する必要があり、テストも重複します 修正方法: commonMain(純粋な Kotlin)に移すか、expect/actual を作成します
3. 抽象化の漏れ
問題: commonMain 内のプラットフォームコード
// commonMain - ❌ BAD
import android.content.Context // Won't compile on iOS!
修正方法: expect/actual または依存性注入を使用します
4. 早すぎる抽象化
問題: 2つ目のプラットフォームで必要になる前に expect/actual を作成する
// ❌ BAD - only used on Android currently
expect fun showNotification(...)
理由: 抽象化の境界が不適切になり、労力が無駄になります 対策: iOS で実際に必要になるまで待ち、その後に抽象化します
5. 不適切なソースセット
問題: JVM ライブラリを commonMain に配置する
// commonMain - ❌ BAD
import com.fasterxml.jackson.databind.ObjectMapper
理由: Jackson は iOS/web ではコンパイルできません 対策: jvmAndroid に移動するか、kotlinx.serialization に移行します
クイックリファレンス
| コードの種類 | 推奨される場所 | 理由 |
|---|---|---|
| 純粋な Kotlin のビジネスロジック | commonMain | どこでも動作します |
| Nostr プロトコル、NIP | commonMain | コアロジックで、プラットフォーム API を使用しません |
| JVM ライブラリ(Jackson、OkHttp) | jvmAndroid | Android と Desktop のみ |
| 暗号化(プラットフォームによって異なる) | commonMain で expect、各プラットフォームで actual | プラットフォームごとにセキュリティ API が異なります |
| I/O、ロギング | commonMain で expect、各プラットフォームで actual | プラットフォームごとに実装が異なります |
| 状態(ビジネスロジック) | commonMain または commons/jvmAndroid | 再利用可能な StateFlow パターン |
| ViewModel | commons/commonMain/viewmodels/ | StateFlow/SharedFlow とロジックは共有可能で、Compose MP のライフサイクルと互換性があります |
| UI フォーマッター(純粋なもの) | commons/commonMain | 再利用可能で、依存関係はありません |
| UI コンポーネント(シンプルなもの) | commonsUI/commonMain | カード、ボタン、ダイアログ(Compose UI は commons に配置しません) |
| 画面レイアウト | commonsUI | UI は1つにまとめ、ScreenLayoutSpec でウィンドウサイズに適応させます |
| ナビゲーション | commonsUI(エントリーポイントはネイティブ) | ナビゲーションホストとクロームを共有し、Activity/Window がホストします |
| 権限 | プラットフォーム固有のみ | API に互換性がありません |
| プラットフォーム UX(メニューなど) | プラットフォーム固有のみ | ネイティブな操作感が必要です |
関連項目
- references/abstraction-examples.md - 理由を含む、適切/不適切な抽象化の例
- references/source-set-hierarchy.md - Amethyst の例を含む階層の図
- references/expect-actual-catalog.md - 「抽象化した理由」を含む、24 組すべての expect/actual
- references/target-compatibility.md - プラットフォームの制約と将来への対応
スクリプト
scripts/validate-kmp-structure.sh- 不適切な配置を検出し、ソースセットを検証しますscripts/suggest-kmp-dependency.sh- KMP ライブラリの代替候補(ktor、kotlinx.serialization など)を提案します