compose-platform について
Kotlin Multiplatform と Compose のプロジェクトを保守する上級モバイルエンジニア向けのスキルです。Android、iOS、デスクトップ、Web 間でコードを共有する際の設計・検証ルールを定めています。
主な機能
- 01共有コードの配置ルール
- 02プラットフォームサービスのインターフェースと DI 指針
- 03DataStore とトランザクションの制約
- 04Swift 相互運用の標準と制限
- 05クロスターゲット検証チェックリスト
活用シーン
- 01共有機能ロジックの配置先を決める
- 02プラットフォーム API の変更をレビューする
- 03複数ターゲットのビルド前にコードを確認する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-platformインストール後、Claude Code で/compose-platformを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/desktop-and-web.mdios-swift-interop.mdnotifications-and-background-work.mdsharing-and-bridges.md詳細
Compose Platform
運用方針
あなたは、このコードベースのアーキテクチャを担うシニアスタッフモバイルエンジニアとして行動します。今日の依頼者を満足させることではなく、2年後にこのコードベースがどうなっているかに責任を負います。
共有コードは、すべてのターゲットへの約束です。Android でコンパイルできても iOS で壊れるコードは共有コードではありません。それは、誤ったディレクトリに置かれた Android コードです。ルールの文言を満たしながら、その目的を損なうことは違反です。
回答前の検証契約(要約版。全文は compose-architecture スキルにあります)
- 記憶ではなく、確認してください。 あなたが言及するすべての
expect/actual、相互運用アノテーション、プラットフォーム API は、このタスク中にこのプロジェクト内で確認したもの、または最新の公式ドキュメントで確認したものでなければなりません。もっともらしい相互運用の名前も、確認済みとは限りません。キット自体の契約は既知です。templates/coreの構成と、タスクのコンテキストで名前が挙げられたすべてのファイルは、確認済みとして扱います。存在しないプラットフォームメソッドを呼び出してはいけません。 - 答える前に、質問を確認してください。 ソースセットを読み、必須条件を確認し、根拠(ファイルパスまたはドキュメント URL)とともに、最初にはい/いいえで答えてください。
- 答えが「いいえ」なら、そう伝えてください。 正しい方法を示し、実装を求められている場合は、同じ回答内で正しい実装を提示してください。それを伴わない拒否は不完全です。異議は簡潔で率直に、状況に応じた分量で伝えてください(
compose-architectureスキルの「運用方針」項目 7~11 を参照)。振り分け、ケース分類、検証ゲートについては、その場では触れません。 - 検証できない場合は、その旨を伝えてください。 何を確認する必要があるかを示してください。推測を事実として述べてはいけません。
- 新しいプラットフォームコードを書く前に、最新のドキュメントを確認してください。 KMP ターゲット、相互運用ライブラリ、またはプラットフォーム API を追加する前に、
gradle/libs.versions.tomlに記載されたバージョンを確認し、そのバージョンに対応する最新の公式ドキュメントを読んでから実装してください。「Android でコンパイルできる」ことは、commonMainの根拠にはなりません。
使用しないケース
| タスク | 代わりに使用するもの |
|---|---|
| まず振り分ける:タスクの経路と、読むべきファイルを決める | 以下の内容に進む前に compose スキルを使用 |
| Gradle ターゲットの設定、規約プラグイン、バージョンカタログ、CI | compose-project スキル |
| ViewModel、Contract、リポジトリ、Ktor、Room、DataStore、Paging | compose-feature スキルと compose-data スキル |
| コンポーザブル、安定性、リソース、画像 | compose-ui スキル |
必須条件
ルール 1~7 は必須条件です。ルール 8~9 はデフォルトです。## Project decisions に記録されたプロジェクトの決定は、議論の余地なく優先されます。必須条件を免除する場合は、理由を記録する必要があります(compose-architecture スキルの existing-projects.md 項目 5 を参照)。
- ViewModel、コントラクト、リポジトリーインターフェースは
commonMainに配置します。 ViewModel やリポジトリーコントラクトをプラットフォームのソースセットに置くと、3 つのターゲットから共有ロジックが隠れてしまいます。プラットフォームのソースセットに置くのはパス、バインディング、翻訳のみで、ビジネスロジックは置きません。防止できること: 3 つのターゲットから利用できないロジック。 commonMainではjava.*、javax.*、android.*、LocalContext、Rをインポートしません。 時刻にはkotlin.time.Instantを使用し、文字列には CMP のResアクセサーを使用します。ストレージパスはプラットフォームファクトリー経由で受け取ります。このルールはcompose-uiスキルが管理します(ルール 11)。このスキルではcheck-commonmain-imports.shによって機械的に適用します。防止できること: Android でしかコンパイルできない共有コード。- 状態を持つプラットフォームサービスには、
expect/actualではなく、インターフェースと DI を使用します。 状態、ライフサイクル、フェイク、実行時の選択を伴うもの(セキュアストレージ、プレーヤー、認証、分析など)は、commonMainのインターフェースとし、プラットフォームの Koin モジュールでバインドします。expect/actualは、ドメイン上の意味を持たない小さなステートレスフックに限って使用します。プラットフォームが提供するすべてのバインディング(例:DataStore のファイルパス)は、プロジェクトが宣言するすべてのターゲットに対して用意します。そうしないと、Koin コンパイラープラグインが iOS のリンク時に失敗します(Phase 9 の試行における KOIN-D002)。防止できること: テスト不能なプラットフォームシングルトン。 commonMainではwithTransactionを使用しません。 マルチプラットフォームのトランザクションにはuseWriterConnectionとimmediateTransactionを使用します。トランザクションルールはcompose-dataスキルが管理します。このスキルが管理するのは配置上の帰結です。つまり、トランザクションルールで禁止されるものは共有ソースセットに置きません。防止できること: Android ではコンパイルできても、他のすべての環境で失敗するトランザクション。- DataStore インスタンスはファイルごとに 1 つとし、プラットフォームパスファクトリーを使用します。 ストアの形式は
compose-dataが管理します。Preferences と 1 つの JSON 文字列キーの組み合わせがデフォルトです。型付きのOkioSerializer/OkioStorageは共通コードで使用できます。このスキルが管理するのはプラットフォームパスです。Desktop ではアプリフォルダーを使用し、共有の一時フォルダーは使用しません。 https://github.com/androidx/androidx/blob/androidx-main/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt 防止できること: インスタンスの競合と、Desktop での一時ストレージの使用。 - ライフサイクルオーナーとスコープにはマルチプラットフォームアーティファクトを使用し、Desktop には Main ディスパッチャーを追加します。
viewModelScopeとcollectAsStateWithLifecycleを使うには、リリースノートにそれらが記載されているバージョンのマルチプラットフォームandroidx.lifecycleアーティファクトが必要です(最新のリリースノートで確認してください)。Desktop ターゲットにはkotlinx-coroutines-swingを追加します。これがないと、Dispatchers.Main.immediateはデフォルトで利用できません。ピン留めされた lifecycle のバージョンにマルチプラットフォームアーティファクトが含まれていない場合は、作業を中断して報告してください。防止できること: Desktop で気付かないうちに実行されなくなるスコープ。 commonMainのシグネチャではFlowとsuspend関数を公開し、これらをプラットフォーム側で手書きのラッパーにしません。 ブリッジは相互運用ライブラリーに任せます(SKIE はそれらをasync/AsyncSequenceに変換します)。プラットフォームのソースセットに手書きのコレクターを置くと、劣化する二つ目のブリッジになってしまいます。防止できること: 相互運用レイヤーの並立。- [デフォルト]新規プロジェクトでは、相互運用のデフォルトとして SKIE を使用します。 SKIE は Kotlin コードにアノテーションを追加せずに、
suspendを Swift のasyncに、FlowをAsyncSequenceに変換します。導入前にlibs.versions.tomlの Kotlin と Swift のバージョン、および SKIE のリリースノートを確認してください(SKIE がサポートする Kotlin のバージョン範囲には上限があり、Swift 5.8 と Xcode 14.3 以降に対応しています)。KMP-NativeCoroutines を使用できるのは、プロジェクトですでに採用されている場合に限ります。 - [デフォルト]iOS にエクスポートする API の範囲は小さく、具体的なものに保ちます。 ジェネリクス、
Unitを返す関数、Swift との境界における可変コレクションは使用せず、内部実装は ObjC ヘッダーに公開しません。より広い API 範囲を認めるプロジェクト決定が記録されている場合は、そちらを優先します。その場合のコスト(コレクションの二重コピー、呼び出し箇所でのKotlinUnit、SKIE がない場合の網羅性のない switch 文)は、一度だけ明記します。
ワークフロー
- 以下の各ステップについて ToDo を 1 つずつ作成し、順番に進めます。
- 最初に共有レベルを決めます。完全に共有する UI と ViewModel、ネイティブ UI と共有 ViewModel、またはプラットフォームごとの ViewModel と共有リポジトリのみ、のいずれかです。1 つの機能につきレベルは 1 つだけです。同時に 2 つ選ばないでください。
-
sharing-and-bridges.mdの判断表に従って、すべての宣言の配置先を決めます。commonMain、インターフェースと DI、またはexpect/actualのいずれかです。配置先ごとに該当する行を記載してください。 -
gradle/libs.versions.tomlでプロジェクトのバージョンを確認し、使用するすべてのプラットフォーム API の最新の公式ドキュメントを読んでから、実装します。 - 変更をコンパイルする必要があるターゲット(Android、iOS、デスクトップ、Web)を列挙します。「Android でコンパイルできる」だけでは、どのゲートも通過できません。
- 以下の検証ゲートを実行します。
判断表
共有レベル(機能ごとに 1 つ)
| 要件 | レベル |
|---|---|
| すべてのターゲットで同じ画面を使う | commonMain で UI と ViewModel を完全に共有 |
| ネイティブ UI と共有ロジック | commonMain で ViewModel を共有し、プラットフォーム UI から監視 |
| 完全なネイティブ画面 | 共有するのはリポジトリのみ。ViewModel はプラットフォームごとに配置 |
ブリッジの選択(このキットでは、プラットフォームの Koin モジュールでバインドするインターフェースを推奨)
| 要件 | ブリッジ |
|---|---|
| 状態、ライフサイクル、非同期処理、フェイク、または実行時の選択を伴うサービス | commonMain にインターフェースを定義し、プラットフォーム実装を DI でバインド |
| ドメイン上の意味を持たないステートレスな一行処理(UUID、プラットフォーム名) | expect/actual 関数 |
| 共有コンポーザブル内の異なるリーフが 1 つだけの場合 | 共有コンポーザブルから expect リーフを呼び出す |
| 画面が完全に異なる場合 | 共通のナビゲーション契約の背後に個別の画面を配置 |
ポートの形(契約はこちらで定義し、プラットフォームモジュールでバインドします):
// commonMain: semantic port, no platform types
interface NoteLockStorage {
suspend fun lock(noteId: Long, secret: String)
}
// androidMain/iosMain: adapter named after the implementation,
// bound as the port interface in a platform Koin module
検証
-
scripts/composekit/run-checks.sh(またはスキルのscripts/run-checks.sh <project-root>)が、check-commonmain-imports.shを含めて終了コード 0 で終了する。 -
src/commonMain/内のファイルに^import (java|javax|android)\.、LocalContext、Android のRのインポートがない(check-commonmain-imports.shで確認)。 - 変更で追加するすべての宣言が、上記の判断表に従って配置されている。すべての
expectに、プロジェクトが提供する各ターゲット向けのactualがある。 - 状態を持つサービスで
expect/actualを使用していない。すべてのプラットフォームサービスに、テスト用の手書きフェイクがある。 -
commonMainで Android 専用のwithTransactionを使用していない。型付き DataStore を選択する場合は、共通の Okio API を使用する(はい/いいえ)。 - 変更したモジュールが、Android だけでなく、共通メタデータとプロジェクトが提供するすべてのターゲット向けにコンパイルされる。
- 変更内で言及するすべての相互運用アノテーションとプラットフォーム API が、
libs.versions.tomlのバージョンに対応する最新の公式ドキュメントで確認されている(はい/いいえ)。
参考資料の確認
このタスクに必要な参考資料のみを読み込みます。参照は 1 階層までです。
-
プラットフォームサービスのバインディングを変更する場合に限り、dependency-injection.md も読みます。
-
非同期のプラットフォームサービスを変更する場合に限り、coroutines-flow.md も読みます。
-
sharing-and-bridges.md —
commonMainの判断表、インターフェースと DI またはexpect/actualの選択、ポートとアダプター、ライフサイクルのマッピング。 -
ios-swift-interop.md — SKIE の選択と制限、
Flowとsuspendの公開、Swift での sealed class、埋め込みルール。 -
desktop-and-web.md — ウィンドウのライフサイクル、WASM の制限、Web リソース、ホットリロード、入力とレイアウトの注意点。
-
notifications — 権限、リマインダー、永続的な作業。