推奨される Kotlin Multiplatform プロジェクト構造
基本および高度なプロジェクト構造の概念の概要により、ソースセットと依存関係管理についての理解が得られたはずです。 では、ソースセットを整理し、依存関係を利用するモジュールについてはどうでしょうか?
この記事では、具体的に KMP プロジェクトについて説明します。 モジュール化の意思決定に関する一般的な理解については、Android のモジュール化の概要を参照してください。
最適なモジュール構造
最適なモジュール構造は、目的や必要なターゲットによって異なります。 KMP IDE プラグインウィザードの出力をさまざまな構成やターゲットセットで分析することで、デフォルトでプロジェクトがどのように整理されているかを確認できます。
一般的なアプローチは次のように要約できます:
- アプリのエントリポイントは個別のモジュールに含める必要があり、それぞれが必要な共有コードモジュールに依存するようにします。
- 共有コードは一般的にビジネスロジックと UI に分けられ、戦略としては不要な依存関係を避けることです:
- KMP プロジェクトによって生成されるすべてのアプリが、共有ビジネスロジックに加えて共有 UI コードも使用している場合は、すべての共有コードに対して単一の
sharedモジュールだけで十分な場合があります。 - いずれかのアプリの UI がネイティブコードで記述されている場合(例:iOS UI を純粋な Swift で実装したなど)、不要な場所に Compose Multiplatform の依存関係が入るのを避けるために、UI コードをビジネスロジックから分離するのが合理的です。 その場合、
sharedLogicモジュールとsharedUIモジュールを用意し、必要に応じてエントリポイントモジュールに依存関係として追加できます。
- KMP プロジェクトによって生成されるすべてのアプリが、共有ビジネスロジックに加えて共有 UI コードも使用している場合は、すべての共有コードに対して単一の
- プロジェクトにクライアントアプリとロジックを共有すべきサーバーコードが含まれている場合、推奨される構造は以下の通りです:
- エントリポイントモジュールと、上記のように整理されたクライアント共通コードモジュールを含む
appフォルダ。 - サーバー固有のコードを含む
serverモジュール。 - モデルやバリデーションなど、サーバーとクライアント間で共有されるコードのための
coreモジュール。
- エントリポイントモジュールと、上記のように整理されたクライアント共通コードモジュールを含む
プロジェクトが古い構造(アプリのエントリポイントと共有コードが単一のモジュールに含まれている状態)を使用している場合は、以下のガイドに従ってエントリポイントを個別のモジュールに抽出できます。
Android Gradle Plugin 9 以降を使用する予定がある場合は、Android アプリのエントリポイントを共通コードから分離することが必須です。 詳細については、AGP 9 移行に関する記事を参照してください。
アプリエントリポイント用の個別モジュールの作成
推奨される構造への移行を説明するために使用するサンプルプロジェクトは、サンプルのリポジトリの old-project-structure ブランチにある、古い Compose Multiplatform のサンプルです。
この例は、すべての共有コードと KMP エントリポイントを含む単一の Gradle モジュール(composeApp)と、iOS プロジェクトのコードと設定を含む iosApp フォルダで構成されています。
エントリポイントを独自のモジュールに抽出するには、モジュールを作成し、コードを移動し、新しいモジュールと共通コードモジュールの両方の設定を適宜調整する必要があります。
undefined
デスクトップ JVM アプリ
デスクトップアプリモジュールの作成と設定
デスクトップアプリモジュール(desktopApp)を作成するには:
プロジェクトのルートに
desktopAppディレクトリを作成します。そのディレクトリ内に、空の
build.gradle.ktsファイルとsrcディレクトリを作成します。settings.gradle.ktsファイルに次の行を追加して、プロジェクト設定に新しいモジュールを追加します:kotlininclude(":desktopApp")
デスクトップアプリのビルドスクリプトの設定
デスクトップアプリのビルドスクリプトを機能させるには:
gradle/libs.versions.tomlファイルで、Kotlin JVM Gradle プラグインをバージョンカタログに追加します:toml[plugins] kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }desktopApp/build.gradle.ktsファイルで、共有 UI モジュールに必要なプラグインを指定します:kotlinplugins { alias(libs.plugins.kotlinJvm) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }これらのプラグインがすべてルートの
build.gradle.ktsファイルに記述されていることを確認してください:kotlinplugins { alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false // ... }他のモジュールへの必要な依存関係を追加するために、
composeAppビルドスクリプトのcommonMain.dependencies {}ブロックとjvmMain.dependencies {}ブロックから既存の依存関係をコピーします。この例では、最終結果は次のようになります:kotlinkotlin { dependencies { implementation(projects.sharedLogic) implementation(projects.sharedUI) implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) } }デスクトップ固有の設定を含む
compose.desktop {}ブロックを、composeApp/build.gradle.ktsファイルからdesktopApp/build.gradle.ktsファイルにコピーします:kotlincompose.desktop { application { mainClass = "compose.project.demo.MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "compose.project.demo" packageVersion = "1.0.0" } } }メインメニューから Build | Sync Project with Gradle Files を選択するか、エディタの Gradle リフレッシュボタンをクリックします。
コードの移動とデスクトップアプリの実行
設定が完了したら、デスクトップアプリのコードを新しいディレクトリに移動します:
desktopApp/srcディレクトリ内に、新しいmainディレクトリを作成します。composeApp/src/jvmMain/kotlinディレクトリをdesktopApp/src/main/ディレクトリに移動します。 パッケージの座標がcompose.desktop {}の設定と一致していることが重要です。- すべてが正しく設定されていれば、
desktopApp/src/main/.../main.ktファイル内のインポートが機能し、コードがコンパイルされます。 - デスクトップアプリを実行するには、composeApp [jvm] 実行構成を変更します:
- 実行構成のドロップダウンで、Edit Configurations を選択します。
- Gradle カテゴリで composeApp [jvm] 構成を見つけます。
- Gradle project フィールドで、
ComposeDemo:composeAppをComposeDemo:desktopAppに変更します。
- 更新された構成を開始して、アプリが期待通りに動作することを確認します。
- すべてが正しく動作する場合:
composeApp/src/jvmMainディレクトリを削除します。composeApp/build.gradle.ktsファイルからデスクトップ関連のコードを削除します:compose.desktop {}ブロック- Kotlin
sourceSets {}ブロック内のjvmMain.dependencies {}ブロック kotlin {}ブロック内のjvm()ターゲット宣言
Web アプリ
Web アプリモジュールの作成と設定
Web アプリモジュール(webApp)を作成するには:
プロジェクトのルートに
webAppディレクトリを作成します。そのディレクトリ内に、空の
build.gradle.ktsファイルとsrcディレクトリを作成します。settings.gradle.ktsファイルの最後に次の行を追加して、プロジェクト設定に新しいモジュールを追加します:kotlininclude(":webApp")
Web アプリのビルドスクリプトの設定
Web アプリのビルドスクリプトを機能させるには:
webApp/build.gradle.ktsファイルで、共有 UI モジュールに必要なプラグインを指定します:```kotlin plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) } ```これらのプラグインがすべてルートの
build.gradle.ktsファイルに記述されていることを確認してください:kotlinplugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false // ... }JavaScript と Wasm のターゲット宣言を
composeApp/build.gradle.ktsファイルからwebApp/build.gradle.ktsファイルのkotlin {}ブロックにコピーします:kotlinkotlin { js { browser() binaries.executable() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() } }他のモジュールへの必要な依存関係を追加します:
kotlinkotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedLogic) // 必要なエントリポイント API を提供します implementation(compose.ui) } } }メインメニューから Build | Sync Project with Gradle Files を選択するか、エディタの Gradle リフレッシュボタンをクリックします。
コードの移動と Web アプリの実行
設定が完了したら、Web アプリのコードを新しいディレクトリに移動します:
composeApp/src/webMainディレクトリ全体をwebApp/srcディレクトリに移動します。 すべてが正しく設定されていれば、webApp/src/webMain/.../main.ktファイル内のインポートが機能し、コードがコンパイルされます。webApp/src/webMain/resources/index.htmlファイルで、スクリプト名をcomposeApp.jsからwebApp.jsに更新します。- Web アプリを実行するには、composeApp [wasmJs] 実行構成を変更します:
- 実行構成のドロップダウンで、Edit Configurations を選択します。
- Gradle カテゴリで composeApp [wasmJs] 構成を見つけます。
- Gradle project フィールドで、
ComposeDemo:composeAppをComposeDemo:webAppに変更します。
- JavaScript バージョンも実行できるように、composeApp [js] についても同様の手順を繰り返します。
- 実行構成を開始して、アプリが期待通りに動作することを確認します。
- すべてが正しく動作する場合:
composeApp/src/webMainディレクトリを削除します。composeApp/build.gradle.ktsファイルから Web 関連のコードを削除します:- Kotlin
sourceSets {}ブロック内のwebMain.dependencies {}ブロック kotlin {}ブロック内のjs {}およびwasmJs {}ターゲット宣言
- Kotlin
共有モジュールの設定
サンプルアプリでは、UI とビジネスロジックの両方のコードが共有されているため、すべての共通コードを保持するために単一の共有モジュールのみが必要です。composeApp を共通コードモジュールとして再利用するだけで済みます。
Gradle 設定で調整が必要な唯一のこと(エントリポイントモジュールとの接続に関係しないもの)は、新しい Android Library Gradle プラグインです。 新しいプラグインはマルチプラットフォームプロジェクト専用に構築されており、AGP 9 以降を使用するために必要です。
必要な変更は以下の通りです:
gradle/libs.versions.tomlで、Android-KMP ライブラリプラグインをバージョンカタログに追加します:toml[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }composeApp/build.gradle.ktsファイルに、共有 UI モジュールに必要なプラグインを追加します:kotlinplugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }ルートの
build.gradle.ktsファイルに次の行を追加して、プラグインの適用における競合を避けます:kotlinalias(libs.plugins.androidMultiplatformLibrary) apply falsecomposeApp/build.gradle.ktsファイルで、kotlin.androidTarget {}ブロックの代わりにkotlin.androidLibrary {}ブロックを追加します:kotlinandroidLibrary { namespace = "compose.project.demo.composedemo" compileSdk = libs.versions.android.compileSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } androidResources { enable = true } }composeApp/build.gradle.ktsファイルからルートのandroid {}ブロックを削除します。すべてのコードがアプリモジュールに移動されたため、
androidMainの依存関係を削除します:kotlin.sourceSets.androidMain.dependencies {}ブロックを削除します。Android アプリが期待通りに動作していることを確認します。
(任意) 共有ロジックと共有 UI の分離
プロジェクトの一部のターゲットがネイティブ UI を実装している場合、共通コードを sharedLogic モジュールと sharedUI モジュールに分離することをお勧めします。これにより、ネイティブ UI を持つアプリモジュールが、共有コードを使用するために Compose Multiplatform に依存する必要がなくなります。
以下は、同じサンプルアプリに基づいたアプローチの例です。
共有ロジックモジュールの作成
実際にモジュールを作成する前に、何がビジネスロジックであるか、つまり UI とプラットフォームの両方に依存しないコードはどれかを判断する必要があります。 この例では、唯一の候補は currentTimeAt() 関数です。これは、場所とタイムゾーンのペアに対して正確な時刻を返します。 対照的に、Country データクラスは Compose Multiplatform の DrawableResource に依存しているため、UI コードから分離することはできません。
プロジェクトにすでに
sharedモジュールがある場合(たとえば、すべての UI コードを共有していないため)、sharedLogicの代わりにこのモジュールを使用できます。 共有ロジックを UI と明確に区別するために、名前を変更するとよいでしょう。
対応するコードを sharedLogic モジュールに分離します:
プロジェクトのルートに
sharedLogicディレクトリを作成します。そのディレクトリ内に、空の
build.gradle.ktsファイルとsrcディレクトリを作成します。settings.gradle.ktsの最後に次の行を追加して、新しいモジュールを追加します:kotlininclude(":sharedLogic")新しいモジュールの Gradle ビルドスクリプトを設定します。
gradle/libs.versions.tomlファイルで、Android-KMP ライブラリプラグインをバージョンカタログに追加します:toml[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }sharedLogic/build.gradle.ktsファイルで、共有ロジックモジュールに必要なプラグインを指定します:kotlinplugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) }これらのプラグインがルートの
build.gradle.ktsファイルに記述されていることを確認してください:kotlinplugins { alias(libs.plugins.androidMultiplatformLibrary) apply false alias(libs.plugins.kotlinMultiplatform) apply false // ... }sharedLogic/build.gradle.ktsファイルで、この例で共通モジュールがサポートすべきターゲットを指定します:kotlinkotlin { // sharedLogic はフレームワークとしてエクスポートされず、 // 'sharedUI' のみがエクスポートされるため、iOS フレームワークの設定は不要です。 iosArm64() iosSimulatorArm64() jvm() js { browser() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() } }Android については、
androidTarget {}ブロックの代わりに、androidLibrary {}設定をkotlin {}ブロックに追加します:kotlinkotlin { // ... androidLibrary { namespace = "com.jetbrains.greeting.demo.sharedLogic" compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } } }composeAppで宣言されているのと同じ方法で、共通ソースセットと JavaScript ソースセットに必要な時刻の依存関係を追加します:kotlinkotlin { sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") } webMain.dependencies { implementation(npm("@js-joda/timezone", "2.22.0")) } } }メインメニューから Build | Sync Project with Gradle Files を選択するか、エディタの Gradle リフレッシュボタンをクリックします。
最初に特定したビジネスロジックコードを移動します:
sharedLogic/src内にcommonMain/kotlinディレクトリを作成します。commonMain/kotlin内にCurrentTime.ktファイルを作成します。currentTimeAt関数を元のApp.ktからCurrentTime.ktに移動します。
新しい場所にある関数を
App()コンポーザブルで使用できるようにします。 これを行うには、composeApp/build.gradle.ktsファイルでcomposeAppとsharedLogicの間の依存関係を宣言します:kotlincommonMain.dependencies { implementation(projects.sharedLogic) }再度 Build | Sync Project with Gradle Files を実行して変更を適用します。
composeApp/commonMain/.../App.ktファイルで、currentTimeAt()関数をインポートしてコードを修正します。アプリケーションを実行して、新しいモジュールが正しく機能することを確認します。
これで、共有ロジックを別のモジュールに分離し、クロスプラットフォームで使用することに成功しました。 次のステップ:共有 UI モジュールの作成。
共有 UI モジュールの作成
共通の UI 要素を実装する共有コードを sharedUI モジュールに抽出します:
プロジェクトのルートに
sharedUIディレクトリを作成します。そのディレクトリ内に、空の
build.gradle.ktsファイルとsrcディレクトリを作成します。settings.gradle.ktsの最後に次の行を追加して、新しいモジュールを追加します:kotlininclude(":sharedUI")新しいモジュールの Gradle ビルドスクリプトを設定します:
sharedLogicモジュールでまだ行っていない場合は、gradle/libs.versions.tomlで Android-KMP ライブラリプラグインをバージョンカタログに追加します:toml[plugins] androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }sharedUI/build.gradle.ktsファイルで、共有 UI モジュールに必要なプラグインを指定します:kotlinplugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) }これらのプラグインがすべてルートの
build.gradle.ktsファイルに記述されていることを確認してください:kotlinplugins { alias(libs.plugins.androidMultiplatformLibrary) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinMultiplatform) apply false // ... }kotlin {}ブロックで、この例で共有 UI モジュールがサポートすべきターゲットを指定します:kotlinkotlin { listOf( iosArm64(), iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { // これは Swift コードでインポートする // iOS フレームワークの名前です。 baseName = "sharedUI" isStatic = true } } jvm() js { browser() binaries.executable() } @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() binaries.executable() } }Android については、
androidTarget {}ブロックの代わりに、androidLibrary {}設定をkotlin {}ブロックに追加します:kotlinkotlin { // ... androidLibrary { namespace = "com.jetbrains.greeting.demo.sharedUI" compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() compilerOptions { jvmTarget = JvmTarget.JVM_11 } // Compose Multiplatform リソースを Android アプリで使用できるようにします androidResources { enable = true } } }composeAppで宣言されているのと同じ方法で、共有 UI に必要な依存関係を追加します:kotlinkotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedLogic) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.androidx.lifecycle.viewmodelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") } } }メインメニューから Build | Sync Project with Gradle Files を選択するか、エディタの Gradle リフレッシュボタンをクリックします。
sharedUI/src内に新しいcommonMain/kotlinディレクトリを作成します。リソースファイルを
sharedUIモジュールに移動します:composeApp/commonMain/composeResourcesディレクトリ全体をsharedUI/commonMain/composeResourcesに再配置する必要があります。sharedUI/src/commonMain/kotlinディレクトリ内に、新しいApp.ktファイルを作成します。元の
composeApp/src/commonMain/.../App.ktの内容全体を新しいApp.ktファイルにコピーします。古い
App.ktファイル内のすべてのコードを一時的にコメントアウトします。 これにより、古いコードを完全に削除する前に、共有 UI モジュールが動作しているかどうかをテストできます。新しい
App.ktファイルは、リソースのインポートを除いて期待通りに動作するはずです。リソースは別のパッケージに配置されました。 正しいパスでResオブジェクトとすべての描画可能リソースを再インポートします。例:新しい
App()コンポーザブルを、それに依存するアプリモジュールのエントリポイントで使用できるようにするために、対応するbuild.gradle.ktsファイルに依存関係を追加します:kotlinkotlin { sourceSets { commonMain.dependencies { implementation(projects.sharedUI) // ... } } }アプリを実行して、新しいモジュールがアプリのエントリポイントに共有 UI コードを正常に提供していることを確認します。
composeApp/src/commonMain/.../App.ktファイルを削除します。
これで、クロスプラットフォーム UI コードを専用のモジュールに正常に移動できました。
iOS 統合の更新
iOS アプリのエントリポイントは個別の Gradle モジュールとして構築されていないため、ソースコードを任意のモジュールに埋め込むことができます。 この例では、shared 内に残すことができます:
composeApp/src/iosMainディレクトリをshared/srcディレクトリに移動します。sharedモジュールによって生成されたフレームワークを消費するように Xcode プロジェクトを設定します:File | Open Project in Xcode メニュー項目を選択します。
Project navigator ツールウィンドウで iosApp プロジェクトをクリックし、Build Phases タブを選択します。
Compile Kotlin Framework フェーズを見つけます。
./gradlewで始まる行を見つけ、composeAppをsharedUiに置き換えます:text./gradlew :shared:embedAndSignAppleFrameworkForXcodeContentView.swiftファイル内のインポートは、モジュールの実際の名前ではなく、iOS ターゲットの Gradle 設定のbaseNameパラメータと一致するため、そのままにする必要があることに注意してください。shared/build.gradle.ktsファイルでフレームワーク名を変更した場合は、それに応じてインポートディレクティブを変更する必要があります。
Xcode から、または IntelliJ IDEA の iosApp 実行構成を使用してアプリを実行します。
