メインコンテンツまでスキップ

マルチプラットフォームプロジェクト構造の高度な概念

この記事では、Kotlin Multiplatformプロジェクトの高度な概念とそのGradle実装へのマッピングについて説明します。この情報は、Gradleビルドの低レベルな抽象化(構成、タスク、パブリケーションなど)を扱う必要がある場合や、Kotlin Multiplatformビルド用のGradleプラグインを作成する場合に役立ちます。

このページは、以下の場合に役立ちます。

  • Kotlinがソースセットを作成しないターゲット間でコードを共有する必要がある場合。
  • Kotlin Multiplatformビルド用のGradleプラグインを作成したい場合、または構成、タスク、パブリケーションなど、Gradleビルドの低レベルな抽象化を扱う必要がある場合。

マルチプラットフォームプロジェクトにおける依存関係管理について理解する上で重要なことの1つは、Gradleスタイルのプロジェクトまたはライブラリの依存関係と、Kotlinに固有のソースセット間のdependsOn関係の違いです。

  • dependsOnは、ソースセット階層を有効にし、一般的にマルチプラットフォームプロジェクトでコードを共有できるようにする、共通ソースセットとプラットフォーム固有のソースセット間の関係です。デフォルトのソースセットの場合、階層は自動的に管理されますが、特定の状況では変更する必要がある場合があります。
  • 一般的なライブラリとプロジェクトの依存関係は通常どおりに機能しますが、マルチプラットフォームプロジェクトでそれらを適切に管理するには、コンパイルに使用される粒度の細かいソースセット → ソースセットの依存関係にGradleの依存関係がどのように解決されるかを理解する必要があります。
注記

高度な概念に入る前に、マルチプラットフォームプロジェクト構造の基本を学ぶことをお勧めします。

dependsOnとソースセット階層

通常は、dependsOn 関係ではなく、_依存関係_を扱います。ただし、dependsOnを調べることは、Kotlin Multiplatformプロジェクトがどのように機能するかを理解する上で非常に重要です。

dependsOnは、2つのKotlinソースセット間のKotlin固有の関係です。これは、共通ソースセットとプラットフォーム固有のソースセット間の接続である可能性があります。たとえば、jvmMainソースセットがcommonMainに依存し、iosArm64MainiosMainに依存する場合などです。

KotlinソースセットABの一般的な例を考えてみましょう。式A.dependsOn(B)は、Kotlinに以下を指示します。

  1. Aは、内部宣言を含む、BからのAPIを監視します。
  2. Aは、Bからの期待される宣言に対する実際の実装を提供できます。これは必要十分条件であり、AA.dependsOn(B)が直接または間接的に存在する場合にのみ、Bに対してactualsを提供できます。
  3. Bは、独自のターゲットに加えて、Aがコンパイルされるすべてのターゲットにコンパイルする必要があります。
  4. Aは、Bのすべての通常の依存関係を継承します。

dependsOn関係は、ソースセット階層として知られるツリーのような構造を作成します。以下は、androidTargetiosArm64(iPhoneデバイス)、およびiosSimulatorArm64(Apple Silicon Mac用のiPhoneシミュレーター)を使用したモバイル開発の典型的なプロジェクトの例です。

DependsOn tree structure

矢印はdependsOn関係を表します。 これらの関係は、プラットフォームバイナリのコンパイル中に保持されます。これは、KotlinがiosMaincommonMainからのAPIを見るべきであり、iosArm64Mainからは見るべきではないことを理解する方法です。

DependsOn relations during compilation

dependsOn関係は、KotlinSourceSet.dependsOn(KotlinSourceSet)呼び出しで構成されます。例:

kotlin {
// Targets declaration
sourceSets {
// Example of configuring the dependsOn relation
iosArm64Main.dependsOn(commonMain)
}
}
  • この例は、dependsOn関係がビルドスクリプトでどのように定義できるかを示しています。ただし、Kotlin Gradleプラグインは、デフォルトでソースセットを作成し、これらの関係を設定するため、手動で行う必要はありません。
  • dependsOn関係は、ビルドスクリプトのdependencies {}ブロックとは別に宣言されます。これは、dependsOnが通常の依存関係ではないためです。代わりに、これは、異なるターゲット間でコードを共有するために必要なKotlinソースセット間の特定の関係です。

dependsOnを使用して、公開されたライブラリまたは別のGradleプロジェクトに対する通常の依存関係を宣言することはできません。 たとえば、commonMainkotlinx-coroutines-coreライブラリのcommonMainに依存するように設定したり、commonTest.dependsOn(commonMain)を呼び出したりすることはできません。

カスタムソースセットの宣言

場合によっては、プロジェクトにカスタムの中間ソースセットが必要になる場合があります。 JVM、JS、およびLinuxにコンパイルされるプロジェクトを考えてみましょう。一部のソースをJVMとJSの間でのみ共有したいとします。 この場合、マルチプラットフォームプロジェクト構造の基本で説明されているように、このターゲットのペアに固有のソースセットを見つける必要があります。

Kotlinは、このようなソースセットを自動的に作成しません。つまり、by creating構造を使用して手動で作成する必要があります。

kotlin {
jvm()
js()
linuxX64()

sourceSets {
// Create a source set named "jvmAndJs"
val jvmAndJsMain by creating {
// …
}
}
}

ただし、Kotlinは、このソースセットをどのように扱うか、またはコンパイルするかをまだ知りません。図を描いた場合、 このソースセットは分離され、ターゲットラベルは表示されません。

Missing dependsOn relation

これを修正するには、いくつかのdependsOn関係を追加して、jvmAndJsMainを階層に含めます。

kotlin {
jvm()
js()
linuxX64()

sourceSets {
val jvmAndJsMain by creating {
// Don't forget to add dependsOn to commonMain
dependsOn(commonMain.get())
}

jvmMain {
dependsOn(jvmAndJsMain)
}

jsMain {
dependsOn(jvmAndJsMain)
}
}
}

ここで、jvmMain.dependsOn(jvmAndJsMain)はJVMターゲットをjvmAndJsMainに追加し、jsMain.dependsOn(jvmAndJsMain)はJSターゲットをjvmAndJsMainに追加します。

最終的なプロジェクト構造は次のようになります。

Final project structure

dependsOn関係の手動構成は、デフォルトの階層テンプレートの自動適用を無効にします。 このようなケースとその処理方法の詳細については、追加の構成を参照してください。

他のライブラリまたはプロジェクトへの依存関係

マルチプラットフォームプロジェクトでは、公開されたライブラリまたは別のGradleプロジェクトへの通常の依存関係を設定できます。

Kotlin Multiplatformは、通常、典型的なGradleの方法で依存関係を宣言します。Gradleと同様に、

  • ビルドスクリプトでdependencies {}ブロックを使用します。
  • 依存関係に適切なスコープ(implementationapiなど)を選択します。
  • リポジトリに公開されている場合は座標("com.google.guava:guava:32.1.2-jre"など)を指定するか、同じビルド内のGradleプロジェクトの場合はパス(project(":utils:concurrency")など)を指定して、依存関係を参照します。

マルチプラットフォームプロジェクトの依存関係構成には、いくつかの特別な機能があります。各Kotlinソースセットには、独自のdependencies {}ブロックがあります。これにより、プラットフォーム固有の依存関係をプラットフォーム固有のソースセットで宣言できます。

kotlin {
// Targets declaration
sourceSets {
jvmMain.dependencies {
// This is jvmMain's dependencies, so it's OK to add a JVM-specific dependency
implementation("com.google.guava:guava:32.1.2-jre")
}
}
}

共通の依存関係はより複雑です。マルチプラットフォームライブラリ(たとえば、kotlinx.coroutines)への依存関係を宣言するマルチプラットフォームプロジェクトを考えてみましょう。

kotlin {
androidTarget() // Android
iosArm64() // iPhone devices
iosSimulatorArm64() // iPhone simulator on Apple Silicon Mac

sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}

依存関係の解決には、3つの重要な概念があります。

  1. マルチプラットフォームの依存関係は、dependsOn構造を下に伝播されます。commonMainに依存関係を追加すると、commonMainで直接または間接的にdependsOn関係を宣言するすべてのソースセットに自動的に追加されます。

    この場合、依存関係は実際にすべての*Mainソースセット(iosMainjvmMainiosSimulatorArm64Main、およびiosX64Main)に自動的に追加されました。これらのすべてのソースセットは、commonMainソースセットからkotlin-coroutines-core依存関係を継承するため、それらすべてに手動でコピーアンドペーストする必要はありません。

    Propagation of multiplatform dependencies

    伝播メカニズムを使用すると、特定のソースセットを選択することで、宣言された依存関係を受け取るスコープを選択できます。 たとえば、AndroidではなくiOSでkotlinx.coroutinesを使用する場合は、この依存関係をiosMainにのみ追加できます。

  2. _ソースセット → マルチプラットフォームライブラリ_の依存関係(上記のcommonMainからorg.jetbrians.kotlinx:kotlinx-coroutines-core:1.7.3など)は、依存関係解決の中間状態を表します。解決の最終状態は、常に_ソースセット → ソースセット_の依存関係によって表されます。

    最終的な_ソースセット → ソースセット_の依存関係は、dependsOn関係ではありません。

    粒度の細かい_ソースセット → ソースセット_の依存関係を推測するために、Kotlinは各マルチプラットフォームライブラリとともに公開されるソースセット構造を読み取ります。このステップの後、各ライブラリは全体としてではなく、そのソースセットのコレクションとして内部的に表されます。kotlinx-coroutines-coreの例を以下に示します。

    Serialization of the source set structure
  3. Kotlinは、各依存関係の関係を取得し、それを依存関係からのソースセットのコレクションに解決します。 そのコレクション内の各依存関係ソースセットは、_互換性のあるターゲット_を持つ必要があります。依存関係ソースセット は、コンシューマーソースセットと_少なくとも同じターゲット_にコンパイルされる場合に、互換性のあるターゲットを持ちます。

    サンプルプロジェクトのcommonMainandroidTargetiosX64、およびiosSimulatorArm64にコンパイルされる例を考えてみましょう。

    • まず、kotlinx-coroutines-core.commonMainへの依存関係を解決します。これは、kotlinx-coroutines-coreが 可能なすべてのKotlinターゲットにコンパイルされるためです。したがって、そのcommonMainは、必要なandroidTargetiosX64、およびiosSimulatorArm64を含む、可能なすべてのターゲットにコンパイルされます。
    • 次に、commonMainkotlinx-coroutines-core.concurrentMainに依存します。 kotlinx-coroutines-coreconcurrentMainはJSを除くすべてのターゲットにコンパイルされるため、 コンシューマープロジェクトのcommonMainのターゲットに一致します。

    ただし、コルーチンのiosX64Mainのようなソースセットは、コンシューマーのcommonMainと互換性がありません。 iosX64MaincommonMainのターゲットの1つ(つまり、iosX64)にコンパイルされますが、 androidTargetまたはiosSimulatorArm64のいずれにもコンパイルされません。

    依存関係の解決の結果は、kotlinx-coroutines-coreのどのコードが表示されるかに直接影響します。

    Error on JVM-specific API in common code

ソースセット全体の共通依存関係のバージョンの調整

Kotlin Multiplatformプロジェクトでは、共通ソースセットは、klibを生成するために、また構成された各コンパイルの一部として、複数回コンパイルされます。一貫性のあるバイナリを生成するには、共通コードを常にマルチプラットフォーム依存関係の同じバージョンに対してコンパイルする必要があります。Kotlin Gradleプラグインは、これらの依存関係を調整し、有効な依存関係のバージョンが各ソースセットで同じになるようにするのに役立ちます。

上記の例では、androidx.navigation:navigation-compose:2.7.7依存関係をandroidMainソースセットに追加するとします。プロジェクトはcommonMainソースセットのkotlinx-coroutines-core:1.7.3依存関係を明示的に宣言していますが、バージョン2.7.7のCompose NavigationライブラリにはKotlinコルーチン1.8.0以降が必要です。

commonMainandroidMainは一緒にコンパイルされるため、Kotlin Gradleプラグインは2つのバージョンのコルーチンライブラリから選択し、kotlinx-coroutines-core:1.8.0commonMainソースセットに適用します。ただし、共通コードが構成されたすべてのターゲットで一貫してコンパイルされるようにするには、iOSソースセットも同じ依存関係のバージョンに制約する必要があります。したがって、Gradleはkotlinx.coroutines-*:1.8.0依存関係をiosMainソースセットにも伝播します。

Alignment of dependencies among *Main source sets

依存関係は、*Mainソースセットと*Testソースセットの間で個別に調整されます。*TestソースセットのGradle構成には、*Mainソースセットのすべての依存関係が含まれますが、その逆はありません。したがって、メインコードに影響を与えることなく、新しいライブラリバージョンでプロジェクトをテストできます。

たとえば、プロジェクトのすべてのソースセットに伝播される*MainソースセットにKotlinコルーチン1.7.3の依存関係があるとします。 ただし、iosTestソースセットでは、新しいライブラリリリースをテストするために、バージョンを1.8.0にアップグレードすることにしました。 同じアルゴリズムに従って、この依存関係は*Testソースセットのツリー全体に伝播されるため、すべての*Testソースセットはkotlinx.coroutines-*:1.8.0依存関係でコンパイルされます。

Test source sets resolving dependencies separately from the main source sets

コンパイル

シングルプラットフォームプロジェクトとは異なり、Kotlin Multiplatformプロジェクトでは、すべてのアートファクトをビルドするために複数のコンパイラ起動が必要です。各コンパイラ起動は、_Kotlinコンパイル_です。

たとえば、iPhoneデバイス用のバイナリが、前述のKotlinコンパイル中にどのように生成されるかを以下に示します。

Kotlin compilation for iOS

Kotlinコンパイルは、ターゲットの下にグループ化されます。デフォルトでは、Kotlinは各ターゲットに対して2つのコンパイルを作成します。1つはプロダクションソース用のmainコンパイル、もう1つはテストソース用のtestコンパイルです。

ビルドスクリプトのコンパイルには、同様の方法でアクセスします。最初にKotlinターゲットを選択し、 次に内部のcompilationsコンテナにアクセスし、最後に名前で必要なコンパイルを選択します。

kotlin {
// Declare and configure the JVM target
jvm {
val mainCompilation: KotlinJvmCompilation = compilations.getByName("main")
}
}