取り組みの背景:Compose Multiplatform が変えるクロスプラットフォーム開発
Kotlin Multiplatform(KMP)がビジネスロジックの共有を実現したのに対し、Compose Multiplatform(CMP) は UI 層そのものを共有できる革命的なフレームワークです。JetBrains が開発した CMP を使えば、iOS・Android・Desktop(JVM)・Web(Wasm)のすべてで同一の Composable 関数を動作させることができます。
2026年現在、CMP は 1.7 系がリリースされ、iOS サポートが Production Stable に到達しました。Airbnb や Autodesk などの大企業も本番採用を進め、Flutter に代わる第一選択肢として注目を集めています。
そしてこの複雑な CMP の設定・実装・最適化を劇的に加速させるのが Google Antigravity です。Antigravity のエージェントは、Gradle のマルチモジュール構成、expect/actual パターン、iOS フレームワーク生成、さらには Compose の状態管理まで深く理解しています。
本記事は KMP の基礎知識がある中〜上級者向けに、CMP のプロダクション実装を Antigravity とともに習得するための完全ガイドです。Antigravity での CMP 基礎を学びたい方は、まず「Kotlin Multiplatform × Antigravity 完全ガイド」をご参照ください。
前提知識・環境準備
必要な知識
- Kotlin の基礎文法(コルーチン・Flow の理解があれば理想的)
- Jetpack Compose の基本(
@Composable、State、ViewModelの概念) - KMP の基本構造(
commonMain・androidMain・iosMainの役割)
開発環境
- Antigravity(最新版)
- Android Studio Meerkat 以降(Android ビルド確認用)
- Xcode 26(iOS ビルド・シミュレータ確認用)
- JDK 17 以上
- Gradle 8.9 以上
Antigravity のターミナルで環境を確認します。
# JDK バージョン確認
java -version
# Gradle バージョン確認(wrapper 使用推奨)
./gradlew --version
# Kotlin バージョン確認
kotlinc -versionCompose Multiplatform の概念:KMP との違いを理解する
KMP と CMP の関係を正確に理解することが、設計ミスを防ぐ最初の一歩です。
KMP(Kotlin Multiplatform)
- ビジネスロジック・データ層・ドメイン層の共有が主目的
- UI は各プラットフォームのネイティブ(SwiftUI / Jetpack Compose)を使用
commonMainでインターフェースを定義しexpect/actualでプラットフォーム実装を提供
CMP(Compose Multiplatform)
- KMP の上位概念。UI 層まで含めて共有する
commonMainに書いた Composable がそのまま iOS・Android・Desktop で動作- プラットフォーム固有の UI が必要な箇所のみ
expect/actualで差し替え
モジュール構成のベストプラクティス
Antigravity エージェントに「CMP のプロダクション向けモジュール構成を提案して」と依頼すると、以下のような構成を自動生成してくれます。
project/
├── composeApp/ ← CMP メインモジュール
│ ├── src/
│ │ ├── commonMain/ ← 全プラットフォーム共通
│ │ │ ├── kotlin/
│ │ │ │ ├── ui/ ← 共有 Composable
│ │ │ │ ├── viewmodel/ ← 共有 ViewModel
│ │ │ │ └── di/ ← Koin DI 設定
│ │ │ └── resources/ ← 共有リソース
│ │ ├── androidMain/ ← Android 固有
│ │ ├── iosMain/ ← iOS 固有
│ │ └── desktopMain/ ← Desktop 固有
│ └── build.gradle.kts
├── shared/ ← ビジネスロジック(純粋 KMP)
│ ├── src/
│ │ ├── commonMain/
│ │ ├── androidMain/
│ │ └── iosMain/
│ └── build.gradle.kts
└── settings.gradle.kts
この分離により、composeApp モジュールが UI 責務を、shared モジュールがビジネスロジックを担当する明確な境界を確立できます。
プロジェクトのセットアップ:Antigravity エージェントとの協働
KMP Wizard のセットアップ
kmp.jetbrains.com の Wizard で生成したテンプレートを Antigravity で開きます。Antigravity は .gradle.kts ファイルを開いた瞬間に依存関係を解析し、即座にコード補完とエラー検出を開始します。
Antigravity エージェントによる build.gradle.kts の設定
プロジェクトルートで以下のプロンプトを使うと、最新バージョンの CMP 依存関係を自動設定できます。
@agent
composeApp/build.gradle.kts を開いて、以下を設定してください:
- Compose Multiplatform 1.7.x の最新安定版
- Koin 4.x(DI)
- Ktor 3.x(ネットワーク)
- SQLDelight 2.x(ローカル DB)
- Coil 3.x(画像読み込み)
各バージョンは libs.versions.toml で一元管理してください。
Antigravity は libs.versions.toml(Version Catalog)を自動生成し、build.gradle.kts でのバージョン管理を一元化します。
# libs.versions.toml(Antigravity 生成例)
[versions]
compose-multiplatform = "1.7.0"
kotlin = "2.1.0"
koin = "4.0.0"
ktor = "3.0.3"
sqldelight = "2.0.2"
coil = "3.0.4"
[libraries]
compose-multiplatform = { module = "org.jetbrains.compose:compose-multiplatform-gradle-plugin", version.ref = "compose-multiplatform" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }共有 UI コンポーネントの実装パターン
Design Token による一貫したデザインシステム
commonMain に Design Token を定義することで、全プラットフォームで統一されたデザインを実現します。
// commonMain/kotlin/ui/theme/AppTheme.kt
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val LightColors = lightColorScheme(
primary = Color(0xFF0066FF),
secondary = Color(0xFF6B9FFF),
surface = Color(0xFFF8FAFF),
background = Color(0xFFFFFFFF),
onPrimary = Color.White,
onSurface = Color(0xFF1A1A2E)
)
private val DarkColors = darkColorScheme(
primary = Color(0xFF4D94FF),
secondary = Color(0xFF9BB8FF),
surface = Color(0xFF1A1A2E),
background = Color(0xFF0D0D1A),
onPrimary = Color.White,
onSurface = Color(0xFFE8EEFF)
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
MaterialTheme(
colorScheme = if (darkTheme) DarkColors else LightColors,
content = content
)
}Antigravity の AI はシステムダークテーマの検出を expect/actual で実装するよう自動提案します。
Screen レベルの Composable 設計
CMP では画面単位のコンポーネントを commonMain に集約します。
// commonMain/kotlin/ui/screens/HomeScreen.kt
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun HomeScreen(
viewModel: HomeViewModel = koinViewModel(),
onNavigateToDetail: (String) -> Unit = {}
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(title = { Text("Compose Multiplatform") })
}
) { paddingValues ->
when (val state = uiState) {
is HomeUiState.Loading -> {
Box(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
}
}
is HomeUiState.Success -> {
LazyColumn(
modifier = Modifier.fillMaxSize().padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(state.items) { item ->
ItemCard(item = item, onClick = { onNavigateToDetail(item.id) })
}
}
}
is HomeUiState.Error -> {
ErrorScreen(message = state.message, onRetry = viewModel::reload)
}
}
}
}このコードは 1つの Composable で iOS・Android・Desktop すべて動作します。
プラットフォーム固有処理の分離戦略
CMP の設計上最も重要なのが、expect/actual によるプラットフォーム固有処理の分離です。
expect/actual の設計原則
Antigravity にプロンプトを送ると、どの処理を commonMain に置き、どの処理を expect/actual で分離すべきかを自動判定します。
// commonMain: 共通インターフェース定義
expect class PlatformContext
expect fun getPlatformName(): String
expect fun openUrl(url: String)
// -- Android 実装 --
// androidMain:
actual class PlatformContext(val context: android.content.Context)
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.RELEASE}"
actual fun openUrl(url: String) {
// Android の Intent で外部ブラウザを開く
val intent = android.content.Intent(
android.content.Intent.ACTION_VIEW,
android.net.Uri.parse(url)
)
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
// context は Koin 経由で注入
}
// -- iOS 実装 --
// iosMain:
actual class PlatformContext
actual fun getPlatformName(): String =
"iOS ${platform.UIKit.UIDevice.currentDevice.systemVersion}"
actual fun openUrl(url: String) {
val nsUrl = platform.Foundation.NSURL.URLWithString(url) ?: return
platform.UIKit.UIApplication.sharedApplication.openURL(
nsUrl,
options = emptyMap<Any?, Any?>(),
completionHandler = null
)
}
// -- Desktop 実装 --
// desktopMain:
actual class PlatformContext
actual fun getPlatformName(): String =
"Desktop (${System.getProperty("os.name")} ${System.getProperty("os.version")})"
actual fun openUrl(url: String) {
java.awt.Desktop.getDesktop().browse(java.net.URI(url))
}カメラ・写真アクセスの実装
モバイルアプリではカメラや写真ライブラリへのアクセスが必要になるケースが多いです。Antigravity エージェントに「iOS・Android・Desktop のカメラアクセスを expect/actual で実装して」と依頼すると、moko-media や peekaboo ライブラリを使った実装コードを自動生成します。
// commonMain
expect class ImagePicker {
@Composable
fun launch(onImageSelected: (ByteArray?) -> Unit)
}
// Android: ActivityResultContracts.PickVisualMedia を使用
// iOS: PHPickerViewController を使用
// Desktop: JFileChooser でファイル選択Antigravity エージェント活用:CMP 開発効率化フロー
ViewModel の自動生成
Antigravity の最大の強みは、commonMain に配置可能な ViewModel の自動生成です。
@agent
HomeViewModel を作成してください。
- Ktor でAPIからアイテムリストを取得(URLは https://api.example.com/items)
- SQLDelight でローカルキャッシュを実装
- UiState は Loading/Success/Error の sealed class
- Koin で依存注入
Antigravity は上記プロンプトから、以下の構造を自動生成します。
HomeViewModel.kt(commonMain)HomeUiState.kt(sealed class)ItemRepository.kt(commonMain)ItemRepositoryImpl.kt(commonMain、Ktor + SQLDelight 実装)AppModule.kt(Koin モジュール)HomeScreenTest.kt(unit test)
実際の出力コードには、Ktor のプラットフォーム別 Engine(OkHttp for Android / Darwin for iOS / CIO for Desktop)の設定も含まれています。
Compose Navigation の設定
CMP の Navigation は navigation-compose ライブラリで実装します。
// commonMain/kotlin/navigation/AppNavigation.kt
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.runtime.Composable
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{itemId}") {
fun createRoute(itemId: String) = "detail/$itemId"
}
object Settings : Screen("settings")
}
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToDetail = { itemId ->
navController.navigate(Screen.Detail.createRoute(itemId))
}
)
}
composable(Screen.Detail.route) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") ?: return@composable
DetailScreen(
itemId = itemId,
onBack = { navController.popBackStack() }
)
}
composable(Screen.Settings.route) {
SettingsScreen()
}
}
}Desktop 対応:JVM Desktop ビルドの実践
CMP の大きな差別化点のひとつが、追加コストほぼゼロで Desktop アプリが生成できることです。
Desktop エントリポイントの設定
// desktopMain/kotlin/main.kt
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import androidx.compose.ui.unit.dp
fun main() = application {
val windowState = rememberWindowState(
width = 1200.dp,
height = 800.dp
)
Window(
onCloseRequest = ::exitApplication,
title = "My CMP App",
state = windowState
) {
// 共通の App Composable を呼び出すだけ
AppTheme {
AppNavigation()
}
}
}Desktop 固有のメニューバー実装
Desktop では OS ネイティブのメニューバーを desktopMain に実装します。
// desktopMain/kotlin/main.kt(MenuBar 追加)
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.window.MenuBar
import kotlinx.coroutines.launch
Window(
onCloseRequest = ::exitApplication,
title = "My CMP App"
) {
MenuBar {
Menu("File", mnemonic = 'F') {
Item("New", shortcut = KeyShortcut(Key.N, meta = true)) { /* ... */ }
Item("Open", shortcut = KeyShortcut(Key.O, meta = true)) { /* ... */ }
Separator()
Item("Quit", shortcut = KeyShortcut(Key.Q, meta = true)) { exitApplication() }
}
Menu("Edit", mnemonic = 'E') {
Item("Preferences") { /* ... */ }
}
}
AppTheme { AppNavigation() }
}Desktop 配布パッケージの生成
Antigravity は jpackage タスクを使った配布パッケージ生成も支援します。
# macOS 向け .dmg を生成
./gradlew :composeApp:packageDmg
# Windows 向け .msi を生成
./gradlew :composeApp:packageMsi
# Linux 向け .deb を生成
./gradlew :composeApp:packageDeb生成された .dmg / .msi / .deb は composeApp/build/compose/binaries/ に出力されます。
テスト自動化戦略
commonMain の Unit テスト
CMP では commonTest に配置したテストが全プラットフォームで実行されます。
// commonTest/kotlin/viewmodel/HomeViewModelTest.kt
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
class HomeViewModelTest {
@Test
fun `初期状態はLoadingであること`() = runTest {
val fakeRepository = FakeItemRepository()
val viewModel = HomeViewModel(fakeRepository)
assertIs<HomeUiState.Loading>(viewModel.uiState.value)
}
@Test
fun `アイテム取得成功時はSuccessStateになること`() = runTest {
val fakeItems = listOf(
Item(id = "1", title = "テストアイテム1"),
Item(id = "2", title = "テストアイテム2")
)
val fakeRepository = FakeItemRepository(items = fakeItems)
val viewModel = HomeViewModel(fakeRepository)
viewModel.reload()
val state = viewModel.uiState.value
assertIs<HomeUiState.Success>(state)
assertEquals(2, state.items.size)
}
@Test
fun `API エラー時はErrorStateになること`() = runTest {
val fakeRepository = FakeItemRepository(shouldThrow = true)
val viewModel = HomeViewModel(fakeRepository)
viewModel.reload()
assertIs<HomeUiState.Error>(viewModel.uiState.value)
}
}Compose UI テスト(Android)
// androidTest での UI テスト
@RunWith(AndroidJUnit4::class)
class HomeScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun ローディング中はインジケータが表示される() {
composeTestRule.setContent {
AppTheme {
HomeScreen(viewModel = FakeLoadingViewModel())
}
}
composeTestRule.onNodeWithTag("loadingIndicator").assertIsDisplayed()
}
}Antigravity エージェントは Fake の自動生成も行います。「HomeViewModel のテスト用 FakeRepository を作って」と依頼するだけで、適切なスタブが生成されます。
本番リリースフロー
iOS リリース(App Store)
CMP で生成された .xcframework を Xcode プロジェクトに組み込みます。
# iOS Framework 生成
./gradlew :composeApp:linkReleaseFrameworkIosArm64
# Xcode Archive → Validate → Distribute App
# Fastlane を使った自動化も可能Antigravity の Fastlane 連携を使うと、以下のコマンド1つでビルドからアップロードまで完了します。
@agent
Fastlane の Appfile と Matchfile を設定して、
iOS の TestFlight 自動デプロイ lane を作成してください。
証明書は App Store Connect API キー認証を使用すること。
Android リリース(Google Play)
# AAB(Android App Bundle)生成
./gradlew :composeApp:bundleRelease
# 署名(keystore 使用)
jarsigner -verbose -sigalg SHA256withRSA \
-digestalg SHA-256 \
-keystore release.keystore \
composeApp/build/outputs/bundle/release/composeApp-release.aab \
release-key-aliasDesktop 配布(GitHub Releases)
# .github/workflows/desktop-release.yml(Antigravity 自動生成)
name: Desktop Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Desktop Package
run: |
if [ "${{ runner.os }}" == "macOS" ]; then
./gradlew :composeApp:packageDmg
elif [ "${{ runner.os }}" == "Windows" ]; then
./gradlew :composeApp:packageMsi
else
./gradlew :composeApp:packageDeb
fi
shell: bash
- name: Upload Release Asset
uses: actions/upload-artifact@v4
with:
name: desktop-${{ runner.os }}
path: composeApp/build/compose/binaries/**/*よくあるエラーと対処法
CMP 開発でよく遭遇するエラーと、Antigravity を使った解決策をまとめます。
エラー1: Unresolved reference: actual のコンパイルエラー
e: ... error: unresolved reference: PlatformContext
原因は expect 宣言に対応する actual が未実装なプラットフォームがある場合です。Antigravity にエラーメッセージをペーストすると、「desktopMain に actual class PlatformContext が不足しています」と即座に指摘し、実装テンプレートを生成します。
エラー2: iOS ビルドで Framework not found エラー
Xcode のビルド設定で FRAMEWORK_SEARCH_PATHS に $(SRCROOT)/../composeApp/build/cocoapods/framework/ を追加する必要があります。Antigravity は Podfile の設定ミスも検出します。
エラー3: Desktop で ClassNotFoundException が発生
Desktop ビルドでは Shadow JAR の設定が必要な場合があります。
// composeApp/build.gradle.kts
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "MyApp"
packageVersion = "1.0.0"
// Desktop 固有の JVM オプション
jvmArgs("-Xmx512m")
}
}
}エラー4: iOS で画像リソースが表示されない
CMP の画像リソースは commonMain/composeResources/drawable/ に配置する必要があります。androidMain/res/drawable/ に置いても iOS では参照できません。Antigravity はリソースパスのミスを静的解析で検出します。
まとめ
Compose Multiplatform は、1つの Kotlin コードベースで iOS・Android・Desktop をカバーする、2026年最も注目すべきクロスプラットフォームフレームワークです。本記事で解説した主なポイントをまとめます。
- モジュール構成:
composeApp(UI共有)とshared(ビジネスロジック)の明確な分離 - expect/actual: プラットフォーム固有処理の適切な分離と、Antigravity による自動生成
- 共有 Composable: テーマ・Navigation・ViewModel を
commonMainに集約 - Desktop 対応:
packageDmg/packageMsi/packageDebでネイティブアプリを配布 - テスト:
commonTestに書いたテストが全プラットフォームで実行 - リリースパイプライン: Fastlane + GitHub Actions で iOS/Android/Desktop を一括自動化
Antigravity の AI エージェントは CMP の複雑な設定を大幅に簡略化し、expect/actual の生成・テストのスタブ・ビルド設定の最適化を自動で行います。CMP と Antigravity の組み合わせは、個人開発者がひとつのコードベースで3プラットフォームをリリースするという目標を、かつてないほど現実的なものにしています。
CMP プロジェクトの状態管理をさらに深く理解したい方には、「Kotlin × Coroutines/Flow/Room/Hilt 完全ガイド」もあわせてご参照ください。
プロダクション実装に必要な知識が体系的にまとめられています。