mcpField
K
kotlin-multiplatform

kotlin-multiplatform

github.com/vitorpamplona開発1.6k1.6k 回インストール今日更新
リポジトリ

kotlin-multiplatform について

kotlin-multiplatformは、AmethystのKotlin Multiplatformアーキテクチャを検討するためのガイダンススキルです。共有ソースセットに含めるコードとプラットフォーム固有に保つコードの判断を、コードベースの例や現行・将来のターゲット向けの指針とともに支援します。

主な機能

  1. 01共有コードとプラットフォーム固有コードの判断
  2. 02ソースセット依存関係の指針
  3. 03expect/actualの使用パターン
  4. 04JVMとターゲット互換性の指針
  5. 05抽象化に関する一般的な落とし穴

活用シーン

  1. 01新しいコードのソースセット選択
  2. 02commonMain内のプラットフォームAPI確認
  3. 03Android、Desktop、iOS間のコード共有計画
  4. 04将来のwebやwasmターゲット向け抽象化の評価

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vitorpamplona/amethyst --skill kotlin-multiplatform

インストール後、Claude Code で/kotlin-multiplatformを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
abstraction-examples.md
expect-actual-catalog.md
source-set-hierarchy.md
target-compatibility.md
scripts/
suggest-kmp-dependency.sh
validate-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 イベントループを使用します)

将来への備えのヒント:

  1. commonMain では純粋な Kotlin を優先します
  2. kotlinx.* ライブラリを使用します:
    • Jackson の代わりに kotlinx.serialization
    • OkHttp の代わりに ktor(ktor は Web をサポートします)
    • 独自の日付処理の代わりに kotlinx.datetime
  3. ファイル操作に platform.posix を使用しないでください
  4. 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 プロトコル、NIPcommonMainコアロジックで、プラットフォーム API を使用しません
JVM ライブラリ(Jackson、OkHttp)jvmAndroidAndroid と Desktop のみ
暗号化(プラットフォームによって異なる)commonMain で expect、各プラットフォームで actualプラットフォームごとにセキュリティ API が異なります
I/O、ロギングcommonMain で expect、各プラットフォームで actualプラットフォームごとに実装が異なります
状態(ビジネスロジック)commonMain または commons/jvmAndroid再利用可能な StateFlow パターン
ViewModelcommons/commonMain/viewmodels/StateFlow/SharedFlow とロジックは共有可能で、Compose MP のライフサイクルと互換性があります
UI フォーマッター(純粋なもの)commons/commonMain再利用可能で、依存関係はありません
UI コンポーネント(シンプルなもの)commonsUI/commonMainカード、ボタン、ダイアログ(Compose UI は commons に配置しません)
画面レイアウトcommonsUIUI は1つにまとめ、ScreenLayoutSpec でウィンドウサイズに適応させます
ナビゲーションcommonsUI(エントリーポイントはネイティブ)ナビゲーションホストとクロームを共有し、Activity/Window がホストします
権限プラットフォーム固有のみAPI に互換性がありません
プラットフォーム UX(メニューなど)プラットフォーム固有のみネイティブな操作感が必要です

関連項目

スクリプト

  • scripts/validate-kmp-structure.sh - 不適切な配置を検出し、ソースセットを検証します
  • scripts/suggest-kmp-dependency.sh - KMP ライブラリの代替候補(ktor、kotlinx.serialization など)を提案します

コメント