mcpField
C
compose-multiplatform

compose-multiplatform

github.com/athevon開発698947 回インストール3 週間前に更新
リポジトリ

compose-multiplatform について

Kotlin MultiplatformとCompose Multiplatformによる、Android、iOS、デスクトップ、Web向けUI開発スキルです。Composeコードを共有しながら、プラットフォーム固有の差異や相互運用を扱う開発者向けです。

主な機能

  1. 01KMPとCMPの役割を整理
  2. 02共有コードと固有コードの構成
  3. 03expect/actualによる差異の実装
  4. 04フォント、画像、文字列の共有
  5. 05ComposeとSwiftUI間のビュー埋め込み
  6. 06クロスプラットフォームのアニメーションとジェスチャー

活用シーン

  1. 01マルチプラットフォームComposeの構成
  2. 02共有UIからプラットフォームAPIを利用
  3. 03iOSアプリにCompose UIを組み込む
  4. 04キーボード、インセット、アニメーション差異の調査

インストール

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

$npx skills add https://github.com/athevon/genjutsu --skill compose-multiplatform

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

詳細

バージョンに依存します。 以下に記載するすべての API 名、SDK の条件、ブラウザーサポートに関する記述は、 一次情報をもとに 2026-09-08 に確認済みです。確認対象と確認時期は _jutsu/VERSIONS.md に記載されています。この日付が古い場合は、バージョン番号に基づいて作業する前に再確認してください。

Compose Multiplatform

Compose Multiplatform(CMP)および Kotlin Multiplatform(KMP)を使った、クロスプラットフォーム UI のパターンです。 org.jetbrains.compose プラグインを使用するプロジェクトで読み込まれます。 基礎となるアニメーション API については ../compose-motion/SKILL.md を参照してください。このファイルでは、Android、iOS、Desktop、Web 向けに 1 つの Compose コードベースを書く場合に固有の内容を扱います。


KMP と CMP の違い — 簡単な説明

KMP(Kotlin Multiplatform)は、言語とビルド基盤です。共有 Kotlin コードを JVM、Native(iOS、macOS、Linux、Windows)、Wasm 向けにコンパイルします。CMP(Compose Multiplatform)は、KMP 上に構築された UI フレームワークで、JetBrains が Jetpack Compose を移植したものです。commonMain に Compose コードベースを 1 つ書けば、Android、iOS、Desktop(JVM)、Web(Wasm)で実行できます。プラットフォーム固有のコードは androidMain、iosMain、desktopMain、wasmJsMain に配置し、expect/actual 宣言を通じて組み込みます。


プロジェクト構成

composeApp/
├── src/
│   ├── commonMain/        ← shared Compose code (most of the app)
│   │   └── kotlin/
│   ├── androidMain/       ← Android-specific (uses Activity, Context)
│   ├── iosMain/           ← iOS-specific (uses UIKit/UIView interop)
│   ├── desktopMain/       ← JVM desktop (uses java.awt/swing if needed)
│   └── wasmJsMain/        ← Wasm web target
├── build.gradle.kts
iosApp/                    ← Xcode project consuming the generated framework
androidApp/                ← Android Application module (often merged into composeApp)

適切に設計された CMP プロジェクトでは、コードの 80~95%を commonMain フォルダーに配置します。iosMain または androidMain が数百行を超えて増え始めた場合は、共有できる UI ロジックにプラットフォーム固有の関心事が入り込んでいる可能性があります。


expect/actual パターン

ターゲットごとに異なる実装が本当に必要な場合に使う、KMP の拡張手段です。commonMain で契約を 1 回宣言し、ターゲットごとに 1 回ずつ実装します。

// commonMain
expect fun openShareSheet(text: String)

// androidMain
actual fun openShareSheet(text: String) {
    val intent = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, text)
    }
    context.startActivity(Intent.createChooser(intent, null))
}

// iosMain
actual fun openShareSheet(text: String) {
    val activityVC = UIActivityViewController(
        activityItems = listOf(text),
        applicationActivities = null
    )
    UIApplication.sharedApplication.keyWindow
        ?.rootViewController
        ?.presentViewController(activityVC, true, null)
}

expect/actual は、トップレベル関数、クラス、型エイリアス、プロパティに使用できます。actual のシグネチャは、修飾子やデフォルト値も含めて完全に一致させる必要があります。


コンポーザブルの expect/actual

コンポーザブルにも同じルールが適用されます。プラットフォーム固有の Compose API(Android の RuntimeShader、iOS の UIKitView、Desktop の SwingPanel)が必要な機能で役立ちます。

// commonMain
@Composable
expect fun PlatformBlur(modifier: Modifier = Modifier, content: @Composable () -> Unit)

// androidMain (uses RuntimeShader on Android 13+)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
    Box(modifier.graphicsLayer { renderEffect = blurEffect }) { content() }
}

// iosMain (uses UIVisualEffectView via UIKitView)
@Composable
actual fun PlatformBlur(modifier: Modifier, content: @Composable () -> Unit) {
    Box(modifier) {
        UIKitView(
            factory = {
                UIVisualEffectView(
                    effect = UIBlurEffect.effectWithStyle(
                        UIBlurEffectStyle.UIBlurEffectStyleSystemMaterial
                    )
                )
            },
            modifier = Modifier.matchParentSize()
        )
        content()
    }
}

ルール:expect コンポーザブルは例外とし、通常は使用しないでください。「プラットフォームらしさ」の違いの多くは、別々のコードパスを用意しなくても、commonMain のトークン(色、角丸、スプリングの剛性)で調整できます。


クロスプラットフォームでの LocalDensity

Android では、LocalDensity.current.density はデバイスの DPI バケット(1.0、1.5、2.0、3.0 など)を反映します。iOS では、密度は UIScreen.scale(Retina では通常 2.0 または 3.0)から計算されます。Desktop では、密度は画面の拡大率に依存します(デフォルトは 1.0、Retina クラスのディスプレイでは 2.0、Windows ではユーザーが設定可能です)。Wasm では、密度は window.devicePixelRatio に従います。

Dp とピクセルの比率をハードコードせず、変換は Dp と LocalDensity に任せてください。正確なピクセル値が必要な場合(たとえば Canvas の描画処理)は、明示的に変換します。

val density = LocalDensity.current
val pxValue = with(density) { 16.dp.toPx() }

負荷の高いループ内で density を読み取るのは避け、変換結果をキャッシュしてください。


LocalConfiguration とプラットフォーム対応 UI

LocalConfiguration.current は Android 専用で、androidMain にあります。CMP では、クロスプラットフォームに対応した次の代替手段を優先してください。

  • LocalWindowInfo.current.containerSize — IntSize としてウィンドウ/画面サイズを取得します。commonMain で使用できます。
  • LocalDensity.current — 密度を取得します。commonMain で使用できます。
  • LocalLayoutDirection.current — LTR/RTL のレイアウト方向を取得します。
  • BoxWithConstraints { ... } — レイアウト内で maxWidth/maxHeight を直接取得します。

向き、デバイスの種類、モデルなど、実際のデバイス特性が必要な場合は、アクセスを expect/actual でラップし、PlatformInfo のような型付きオブジェクトを共通レイヤーに渡してください。


Compose Resources によるクロスプラットフォームのフォント

org.jetbrains.compose.resources は共有リソースプラグインです。フォントを commonMain/composeResources/font/ に配置すると、Gradle プラグインが型付きの Res アクセサーを生成します。

composeApp/src/commonMain/composeResources/
├── font/
│   ├── Inter-Regular.ttf
│   └── Inter-Bold.ttf
├── drawable/
│   └── logo.xml            ← Android XML vector. SVG works on every target EXCEPT Android
└── values/
    ├── strings.xml          ← default locale
└── values-fr/
    └── strings.xml          ← French overrides (qualifier goes on the DIRECTORY)

commonMain での使用方法:

import myproject.composeapp.generated.resources.Inter_Regular
import myproject.composeapp.generated.resources.Inter_Bold
import myproject.composeapp.generated.resources.Res

val InterFamily = FontFamily(
    Font(Res.font.Inter_Regular, FontWeight.Normal),
    Font(Res.font.Inter_Bold, FontWeight.Bold),
)

Text("Hello", fontFamily = InterFamily)

Res.drawable.logo(画像)や Res.string.app_name(stringResource(...) を使用したローカライズ文字列)も同じパターンです。例外は生ファイルです。生成される Res.file.* アクセサーはありません。composeResources/files/ 以下のファイルを読み込む場合は、パス文字列をサスペンド関数 Res.readBytes(path) に渡します。たとえば val bytes = Res.readBytes("files/config.json") のようにします。


SwiftUI との iOS 相互運用

CMP は、SwiftUI アプリに組み込める UIViewController を生成します。KMP は、UIViewController を返すトップレベルの Kotlin 関数(一般的には MainViewController() または ComposeUIViewController { ... } という名前)を生成します。これを UIViewControllerRepresentable でラップします。

// iosMain/kotlin/main.ios.kt
fun MainViewController(): UIViewController = ComposeUIViewController {
    AppContent()  // commonMain composable
}
// iOS app target
import SwiftUI
import ComposeApp  // KMP-generated framework

struct ComposeContent: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        Main_iosKt.MainViewController()
    }
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct ContentView: View {
    var body: some View { ComposeContent().ignoresSafeArea() }
}

Kotlin 関数名は、ファイル名が main.ios.kt の場合、Main_iosKt.MainViewController() のようにマングルされます。シンボル名が想定と異なる場合は、生成されたフレームワークヘッダーを確認してください。


Android のエントリーポイント

Android では相互運用のための手続きは不要です。Activity が setContent { ... } を通じて共有コンポーザブルを直接ホストします。

// androidApp/src/main/kotlin/MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppContent()  // commonMain composable
        }
    }
}

Context または Activity を commonMain に渡す必要がある場合は、DI グラフを通じて公開するか、commonMain に expect class PlatformContext を定義し、androidMain に actual class PlatformContext(val context: Context) を定義してください。


Compose iOS ビュー内に SwiftUI/UIKit を埋め込む(逆方向)

UIView ファクトリーには UIKitView を、UIViewController ファクトリーには UIKitViewController を使用します。

// iosMain
UIKitView(
    factory = {
        UISwitch().apply {
            addTarget(target, action = NSSelectorFromString("onToggle:"), forControlEvents = UIControlEventValueChanged)
        }
    },
    modifier = Modifier.size(48.dp, 32.dp)
)

SwiftUI ビューの場合は、Swift の @objc ブリッジ関数を通じて公開される UIHostingController でラップし、生成されたヘッダー(cinterop)を使って Kotlin から呼び出します。詳しいパターンについては references/cmp-interop.md を参照してください。


クロスプラットフォームのアニメーション

すべてのアニメーション API(animate*AsState、AnimatedVisibility、updateTransition、SharedTransitionLayout)は、CMP 1.7 以降、すべてのターゲットで同じように動作します。commonMain に記述したスプリングの調整は、Android と iOS で同じ物理特性になります。ジェスチャー(Modifier.draggable、Modifier.pointerInput)も、同じ API でクロスプラットフォームに動作します。

アニメーションの入門ガイドは ../compose-motion/SKILL.md にあります。クロスプラットフォームで異なる点に注意してください。

  • iOS では初回フレームの表示が遅くなります(Skia の初期化)。200ms の開始アニメーションは Android ではきびきびと感じられますが、iOS のコールドスタート時にはわずかに遅れて感じられます。
  • Wasm のモーションは初回フレームでカクつく場合があります(JIT のウォームアップ)。重要なパスは事前にウォームアップするか、操作可能になるまでモーションを非表示にしてください。

動作しないこと(注意点)

  • iOS でのドロワーの状態:ネイティブの ModalNavigationDrawer で画面端からスワイプして開く操作は、iOS の戻るスワイプジェスチャーと競合します。ボタンで開くか、スワイプ領域を内側に 30dp 以上移動してください。
  • LayoutDirection.Rtl の注意点:Android は RTL をネイティブに処理しますが、iOS の Compose には 1.6 で不具合(テキストの配置、パディングの反転)がありました。1.7 以降で改善されていますが、実際のアラビア語/ヘブライ語の文字列で確認してください。
  • ソフトキーボードの処理:imePadding() / WindowInsets.ime は Android ではそのまま動作し、iOS でも実装されています。CMP に IOSKeyboardEventListener 型はありません。実際に調整するのは ComposeUIViewControllerConfiguration.onFocusBehavior で、デフォルトは OnFocusBehavior.FocusableAboveKeyboard です(CMP が Compose ビュー全体を上方向に移動し、フォーカスされたフィールドがキーボードに隠れないようにします)。imePadding() / WindowInsets.ime を使って自分でレイアウトを調整する場合は、自動移動をオフにしてください。
    // iosMain - imports: androidx.compose.ui.window.ComposeUIViewController,
    //                    androidx.compose.ui.uikit.OnFocusBehavior
    fun MainViewController(): UIViewController = ComposeUIViewController(
        configure = { onFocusBehavior = OnFocusBehavior.DoNothing }
    ) {
        AppContent()
    }
    
    デフォルトを有効にしたまま imePadding() も適用すると、典型的な二重調整の不具合が発生します。コントローラーをホストする SwiftUI 親ビューもキーボードに合わせて調整する場合も同様です。
  • Color.parseHex(...) は Compose に存在しません。Color(0xFFRRGGBB) を使うか、小さな拡張関数を作成してください。
  • Compose 経由での iOS のシステムフォント:FontFamily.SansSerif にフォールバックすれば SF Pro が使われると期待しないでください。iOS の Compose には独自のフォントフォールバックチェーンがあります。ライセンス上問題がなければ Compose Resources 経由で SF Pro をバンドルするか、UIKitView を使ってネイティブの UILabel を配置し、システムフォントのテキストを表示してください。
  • Web(Wasm)でのアニメーション:起動が重くなり、初回フレームで時折カクつきます。ブラウザーの開発者ツールでプロファイリングし、負荷の高いアニメーショングラフは遅延読み込みしてください。
  • java.util.UUID、java.io.File などの JVM 専用 API は、iOS または Wasm 向けに配布する場合、commonMain では使用できません。kotlinx.uuid、kotlinx-io、またはマルチプラットフォーム版の okio を使用してください。

CMP のバージョンに関する注意点

  • 現在の安定版は Compose Multiplatform 1.12.0(2026 年 8 月 25 日リリース)です。以前の長期サポート系列は 1.10.3 でした。クロスプラットフォームでの SharedTransitionLayout、iOS のキーボード処理、iOS の LocalLifecycleOwner に関して 1.7 を過去の最低バージョンとして扱い、対象にするバージョンとはみなさないでください。
  • 1.7 以降、iOS の相互運用機能は androidx.compose.ui.viewinterop(UIKitView、UIKitViewController、UIKitInteropProperties)にあります。interactive: / accessibilityEnabled: パラメーターを持つ旧 androidx.compose.ui.interop.UIKitView オーバーロードは @Deprecated です。
  • 1.10 では UIKitInteropProperties(placedAsOverlay = true) が追加され、相互運用ビューを Compose コンテンツの上に合成できるようになりました。また、SwiftUI ビューを UIHostingController でラップするための固有サイズ(sizeThatFits)サポートも追加されました。
  • K2 コンパイラーを使用する Kotlin 2.x を使ってください。最新の CMP には最新の Kotlin を組み合わせてください(iOS/Web ターゲットでは 2.2.20 以降)。
  • 一部の Material 3 コンポーネントはプラットフォーム固有の外観になります(例:iOS の Switch は iOS 風の比率で自動的に描画されますが、DatePicker はすべてのターゲットで Material の外観を保ちます)。
  • compose-multiplatform-resources プラグインがアセットの標準です。従来の moko-resources は新規プロジェクトでは推奨されなくなりました。

パフォーマンスに関する考慮事項

  • iOS の初回フレームは Android より遅くなります(Skia の起動処理にコールドスタート時で約 150~300ms かかります)。初回のコンポジションが出力されるまでスプラッシュ画面を表示するか、透明なルートコンポーザブルで事前にウォームアップしてください。
  • Wasm バンドルサイズ:圧縮後 2MB 未満を目指してください。重い依存関係をツリーシェイクし、kotlinx.coroutines の遅延コンポジションでセカンダリー画面を遅延読み込みし、wasmJsBrowserDistribution の .wasm 出力を確認してください。
  • デスクトップ:JVM ではコールドスタートが速いです。単一ファイルのバイナリが必要な場合を除き、デスクトップで Kotlin/Native による AOT コンパイルを行うのは過剰です。
  • Android:Jetpack Compose と同じ基準です。Compose コンパイラーの安定性メトリクスと Layout Inspector の再コンポジション回数を使ってプロファイリングしてください。

アンチパターン

悪い例良い例理由
commonMain 内でプラットフォームを検出するためにリフレクションのトリックや System.getProperty("os.name") を使う型付きの Platform オブジェクトと expect/actual を使うリフレクションは Wasm/Native で動作しません。expect/actual はコンパイラーが適用する契約です
Android の Context が commonMain から利用できると想定するexpect class PlatformContext または DI スコープを使って、型付きの依存関係を注入するContext は iOS/Desktop/Wasm には存在しないため、それらのターゲットではコードがコンパイルできません
Android では見栄えがよくても、iOS では違和感のある Material の色をハードコードするcommonMain のデザインシステムを定義し、必要に応じて actual で 2~3 個のトークンを調整するクロスプラットフォームで一貫性を保つのはよいことですが、Material の青が iPhone で場違いに感じられると iOS ユーザーは気づきます
commonMain で LaunchedEffect(Unit) { while(true) { delay(16); ... } } を使うrememberInfiniteTransition() を使うか、ライフサイクルイベントにスコープを限定するコルーチンのタイトなループは iOS でバッテリーを消耗します。無限トランジションは画面外に移動すると一時停止します

クイックリファレンス:サブスキルの読み込み

必要な内容読み込むもの
iOS/Android 相互運用の詳細references/cmp-interop.md
プラットフォームごとの動作カタログreferences/cmp-platform-quirks.md
アニメーション API../compose-motion/SKILL.md
高度なグラフィックス(M3 Expressive、Android 専用の AGSL)../compose-graphics/SKILL.md
SwiftUI との iOS 側ネイティブ相互運用../swiftui-motion/SKILL.md(ターゲットが iOS で、SwiftUI とのネイティブな融合を目指す場合)
モバイル UX の背景知識../mobile-principles/SKILL.md
デスクトップ UX の背景知識../desktop-principles/SKILL.md
基礎知識../motion-principles/SKILL.md

参考資料

コメント