kotlin-multiplatform-expect-actual について
Kotlin Multiplatform 向けに、共通コードとプラットフォーム固有実装の境界設計を解説するスキルです。expect/actual 宣言とインターフェースの選び方を示し、共通 API の意味的な明確さとテスト容易性を保ちます。
主な機能
- 01expect/actual とインターフェースの選択
- 02意味の明確な共通 API の設計
- 03プラットフォーム機構を actual に集約
- 04Compose のプラットフォーム側リーフ指針
- 05ソースセット横断の検証方法
活用シーン
- 01プラットフォーム機能向け共有 API の設計
- 02共通コードへのフェイク可能な依存の追加
- 03プラットフォーム UI と共有 Compose 画面の分離
- 04共通コードへのプラットフォーム型混入のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill kotlin-multiplatform-expect-actualインストール後、Claude Code で/kotlin-multiplatform-expect-actualを入力して読み込まれたか確認します。
詳細
Kotlin Multiplatform:expect/actual の境界
基本原則
共通 API は意味が明確で安定したものにします。プラットフォーム固有の仕組みは、小さな expect/actual 宣言またはインターフェースの背後に置き、Android/iOS/Desktop の詳細を commonMain に持ち込まないようにします。
境界を設ける手順
- 機能を共通の言葉で表現します。例:テキストを共有する、クリップボードを読み取る、触覚フィードバックを要求する、現在の地域を解決する。
- 共通コードの呼び出し元が、フェイク、注入可能な依存関係、ライフサイクルの所有権、または実行時の実装選択を必要とするか確認します。
- 以下の表から最小の境界を選びます。
- 共通のシグネチャにプラットフォームの型や用語を含めないようにします。
- ビジネス上の分岐は共通コードに置き、actual/プラットフォームバインディングは変換レイヤーとして維持します。
- 影響を受けるすべてのソースセットをコンパイルし、可能であればフェイクを使って共通コードをテストし、検証します。
境界を選ぶ
| 状況 | 推奨 |
|---|---|
| 単純なコンパイル時のプラットフォーム別実装 | expect/actual の関数、値、typealias、または末端のコンポーザブル |
| 実装に注入される依存関係、ライフサイクルの所有権、実行時の選択、またはテスト用フェイクが必要 | 共通インターフェースとプラットフォームバインディング |
| UI の大部分は共通で、末端の一部だけが異なる | expect の末端コンポーザブルを呼び出す共通コンポーザブル |
| 画面全体がプラットフォームごとに異なる | 共通のナビゲーション契約の背後にプラットフォーム別の画面を用意 |
| 定数/リソースだけが異なる | 意味を表す値を公開する共通 API と、プラットフォームごとの actual 値 |
共通 API は意味が明確なものにする
呼び出し元がプラットフォーム固有の仕組みではなく、意図を記述するように共通 API を設計します。
// GOOD: common API is semantic
expect fun currentRegion(): Region
// BAD: common API leaks Android implementation
expect fun currentRegionFromAndroidLocale(context: Context): Region
Android の actual では Locale API を使用できます。iOS の actual では Foundation API を使用できます。共通コードの呼び出し元がこれらを知る必要はありません。
actual は薄く保つ
actual の実装では、意味を表す API をプラットフォーム呼び出しに変換します。操作に Activity、view controller、ライフサイクル所有者、DI、またはフェイクが必要なら、そこでいったん立ち止まり、expect class ではなくプラットフォームコードから提供されるインターフェースを使用します。
// commonMain
interface ShareSheet {
suspend fun shareText(text: String)
}
// androidMain
class AndroidShareSheet(
private val activity: Activity,
) : ShareSheet {
override suspend fun shareText(text: String) {
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, text)
activity.startActivity(Intent.createChooser(intent, null))
}
}
Android の実装は、Activity が明示的に所有します。汎用の Context を使うと、UI のライフサイクル要件が隠れてしまうことがよくあります。suspend の意味を定義してください。多くのプラットフォーム UI アクションでは、「シートが起動された」ことを意味し、「ユーザーが共有を完了した」ことを意味するわけではありません。
actual にビジネスルールが増えてきたら、そのルールを共通コードに戻し、actual にはプラットフォームへの変換だけを残します。
テストや DI が重要な場合はインターフェースを優先する
コンパイル時に決まる単純なプラットフォーム API には expect/actual を使います。共通コードでフェイク、複数の実装、実行時の選択、またはライフサイクルの所有権が必要な場合は、インターフェースを使います。
interface Clipboard {
suspend fun setText(text: String)
}
プラットフォームモジュールでは、Clipboard を Android/iOS の実装にバインドします。共通テストではフェイクを使います。
Compose 固有のガイダンス
共通 UI がプラットフォーム固有の末端に到達する場合は、次の点に従います。
- プラットフォーム固有のコンポーザブルは末端ノードに置きます。
- UI を出力するすべての expected コンポーザブルに
Modifierを渡します。 commonMainのシグネチャでプラットフォームの型(Context、Activity、Android のリソース ID、Uri、Bundle、UIViewController、NSBundle、プラットフォーム固有の権限列挙型など)を使用しないようにします。- ネイティブビューのライフサイクルはプラットフォームの actual 内に隠し、適切な相互運用コンテナー(
AndroidView、UIKitViewなど)を使用します。 - コンポーザブルの本体からプラットフォーム処理を直接起動しないでください。共通の Compose コードと同様に、actual のコンポーザブル内で
remember、LaunchedEffect、DisposableEffect、安定したキーを使用します。 - 可能であれば、フェイクのプラットフォームサービスを使って、共通のプレーンな UI コンポーザブルをプレビュー/テストします。
よくある間違い
| 間違い | 修正方法 |
|---|---|
commonMain API が Android/iOS の型を公開している | 意味的な共通型に置き換えます |
expect 関数に特定のプラットフォーム専用のパラメーターがある | その詳細を actual に移します |
| 各 actual でビジネスロジックの分岐が重複している | ビジネスルールを共通コードに移します |
巨大な Platform expect オブジェクトが1つある | Clipboard、ShareSheet、Haptics のように機能ごとに分割します |
| ツリーの上位にプラットフォーム UI が漏れている | プラットフォーム固有の Composable を末端に移します |
| 共通テスト用のフェイク可能な境界がない | expect を直接呼び出す代わりにインターフェースを使います |
| 変更後にコンパイルできるターゲットが1つだけ | 完了前に、影響を受けるすべてのソースセットをコンパイルします |
レビュー時の危険信号
- 共通コードがプラットフォームパッケージをインポートしている。
- actual 実装がプロダクトの状態、ナビゲーションの判断、またはドメインルールを把握している。
- プラットフォーム API の名前が共通関数名に含まれている。
- 3つ目のプラットフォームを追加する際に、共通コードの呼び出し元を変更する必要がある。
- 共通のビジネス動作を検証するだけなのに、テストで Android/iOS のランタイムが必要になる。
関連項目(Compose/共有 UI)
このスキルではプラットフォーム境界に焦点を当てます。共有 UI は、他の Compose ターゲットと同様に接続してください。
kotlin-control-flow—when、ガード条件、網羅性、スマートキャストを使って、共通コードのビジネスロジックの分岐を明示的に保ちます。compose-state-hoisting— 共有のプレーンな UI コンポーザブルと、状態ホルダーの接続方法について説明します。compose-side-effects— actual コンポーザブルでのエフェクトキーとクリーンアップ(LaunchedEffect、DisposableEffectなど)について説明します。compose-modifier-and-layout-styleとcompose-slot-api-pattern— 再利用可能な共有 Compose API(モディファイア、スロット)について説明します。