mcpField
K
kotlin-multiplatform-expect-actual

kotlin-multiplatform-expect-actual

github.com/chrisbanes開発1.1k998 回インストール1 日前に更新
リポジトリ

kotlin-multiplatform-expect-actual について

Kotlin Multiplatform 向けに、共通コードとプラットフォーム固有実装の境界設計を解説するスキルです。expect/actual 宣言とインターフェースの選び方を示し、共通 API の意味的な明確さとテスト容易性を保ちます。

主な機能

  1. 01expect/actual とインターフェースの選択
  2. 02意味の明確な共通 API の設計
  3. 03プラットフォーム機構を actual に集約
  4. 04Compose のプラットフォーム側リーフ指針
  5. 05ソースセット横断の検証方法

活用シーン

  1. 01プラットフォーム機能向け共有 API の設計
  2. 02共通コードへのフェイク可能な依存の追加
  3. 03プラットフォーム UI と共有 Compose 画面の分離
  4. 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 に持ち込まないようにします。

境界を設ける手順

  1. 機能を共通の言葉で表現します。例:テキストを共有する、クリップボードを読み取る、触覚フィードバックを要求する、現在の地域を解決する。
  2. 共通コードの呼び出し元が、フェイク、注入可能な依存関係、ライフサイクルの所有権、または実行時の実装選択を必要とするか確認します。
  3. 以下の表から最小の境界を選びます。
  4. 共通のシグネチャにプラットフォームの型や用語を含めないようにします。
  5. ビジネス上の分岐は共通コードに置き、actual/プラットフォームバインディングは変換レイヤーとして維持します。
  6. 影響を受けるすべてのソースセットをコンパイルし、可能であればフェイクを使って共通コードをテストし、検証します。

境界を選ぶ

状況推奨
単純なコンパイル時のプラットフォーム別実装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 がプラットフォーム固有の末端に到達する場合は、次の点に従います。

  1. プラットフォーム固有のコンポーザブルは末端ノードに置きます。
  2. UI を出力するすべての expected コンポーザブルに Modifier を渡します。
  3. commonMain のシグネチャでプラットフォームの型(Context、Activity、Android のリソース ID、Uri、Bundle、UIViewController、NSBundle、プラットフォーム固有の権限列挙型など)を使用しないようにします。
  4. ネイティブビューのライフサイクルはプラットフォームの actual 内に隠し、適切な相互運用コンテナー(AndroidView、UIKitView など)を使用します。
  5. コンポーザブルの本体からプラットフォーム処理を直接起動しないでください。共通の Compose コードと同様に、actual のコンポーザブル内で remember、LaunchedEffect、DisposableEffect、安定したキーを使用します。
  6. 可能であれば、フェイクのプラットフォームサービスを使って、共通のプレーンな 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(モディファイア、スロット)について説明します。

コメント