マルチプラットフォームプロジェクト構造の高度な概念
この記事では、Kotlin Multiplatformプロジェクト構造の高度な概念と、それらがGradleの実装にどのように対応するかを説明します。この情報は、Gradleビルドの低レベルの抽象化(構成 (configurations)、タスク (tasks)、公開 (publications) など)を扱う必要がある場合や、Kotlin Multiplatformビルド用のGradleプラグインを作成する場合に役立ちます。
このページは、以下のような場合に役立ちます。
- Kotlinがソースセットを作成しないターゲット群の間でコードを共有する必要がある場合。
- Kotlin Multiplatformビルド用のGradleプラグインを作成したい場合、または構成 (configurations)、タスク (tasks)、公開 (publications) など、Gradleビルドの低レベルの抽象化を扱う必要がある場合。
マルチプラットフォームプロジェクトにおける依存関係管理で理解すべき重要な点の1つは、Gradleスタイルのプロジェクトまたはライブラリの依存関係と、Kotlinに特有のソースセット間のdependsOn関係との違いです。
dependsOnは、共通ソースセットとプラットフォーム固有ソースセット間の関係であり、ソースセット階層とマルチプラットフォームプロジェクトでの一般的なコード共有を可能にします。デフォルトのソースセットの場合、階層は自動的に管理されますが、特定の状況で変更する必要がある場合があります。- ライブラリとプロジェクトの依存関係は一般的に通常通り機能しますが、マルチプラットフォームプロジェクトでそれらを適切に管理するには、Gradleの依存関係がどのように解決されるかを理解し、コンパイルに使用される粒度の高いソースセット → ソースセットの依存関係に変換する方法を知る必要があります。
高度な概念に入る前に、マルチプラットフォームプロジェクト構造の基本を学ぶことをお勧めします。
dependsOnとソースセット階層
通常、あなたは_依存関係_を扱い、_dependsOn_関係を扱うことはありません。しかし、dependsOnを調べることは、Kotlin Multiplatformプロジェクトが内部でどのように機能するかを理解するために不可欠です。
dependsOnは、2つのKotlinソースセット間のKotlinに特有の関係です。これは、jvmMainソースセットがcommonMainに依存し、iosArm64MainがiosMainに依存する、といった共通ソースセットとプラットフォーム固有ソースセット間の接続であり得ます。
KotlinソースセットAとBの一般的な例を考えてみましょう。A.dependsOn(B)という表現は、Kotlinに対して以下を指示します。
Aは、内部宣言を含むBのAPIを参照します。Aは、Bの期待される宣言に対してactual実装を提供できます。これは必要十分条件です。なぜなら、AがBに対してactualsを提供できるのは、A.dependsOn(B)が直接的または間接的に存在する場合に限られるからです。Bは、自身のターゲットに加えて、Aがコンパイルするすべてのターゲットに対してもコンパイルされるべきです。Aは、Bのすべての通常の依存関係を継承します。
dependsOn関係は、ソースセット階層として知られるツリーのような構造を作成します。以下は、androidTarget、iosArm64(iPhoneデバイス)、およびiosSimulatorArm64(Apple Silicon Mac用のiPhoneシミュレーター)を含むモバイル開発の典型的なプロジェクトの例です。
矢印はdependsOn関係を表します。 これらの関係は、プラットフォームバイナリのコンパイル中に保持されます。これにより、KotlinはiosMainがcommonMainのAPIを参照するように意図されているが、iosArm64MainのAPIではないことを理解します。
dependsOn関係は、KotlinSourceSet.dependsOn(KotlinSourceSet)呼び出しで構成されます。例えば、以下のように記述します。
kotlin {
// Targets declaration
sourceSets {
// Example of configuring the dependsOn relation
iosArm64Main.dependsOn(commonMain)
}
}- この例は、ビルドスクリプトで
dependsOn関係を定義する方法を示しています。しかし、Kotlin Gradleプラグインはデフォルトでソースセットを作成し、これらの関係を設定するため、手動で行う必要はありません。 dependsOn関係は、ビルドスクリプトのdependencies {}ブロックとは別に宣言されます。これは、dependsOnが通常の依存関係ではなく、異なるターゲット間でコードを共有するために必要なKotlinソースセット間の特定の関係だからです。
公開されたライブラリや他のGradleプロジェクトへの通常の依存関係を宣言するためにdependsOnを使用することはできません。例えば、commonMainをkotlinx-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はまだこのソースセットをどのように扱うか、またはコンパイルするかを知りません。図を描くと、このソースセットは孤立しており、どのターゲットラベルも持たないでしょう。
これを修正するには、いくつかの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に追加します。
最終的なプロジェクト構造は次のようになります。
dependsOn関係を手動で構成すると、デフォルトの階層テンプレートの自動適用が無効になります。追加設定で、そのようなケースとそれらの対処方法について詳しく学んでください。
他のライブラリやプロジェクトへの依存関係
マルチプラットフォームプロジェクトでは、公開されたライブラリまたは他のGradleプロジェクトに通常の依存関係を設定できます。
Kotlin Multiplatformでは、一般的にGradleの典型的な方法で依存関係を宣言します。Gradleと同様に、次のことを行います。
- ビルドスクリプトで
dependencies {}ブロックを使用します。 implementationやapiなど、依存関係に適切なスコープを選択します。- 依存関係を、リポジトリで公開されている場合は
"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つの重要な概念があります。
マルチプラットフォームの依存関係は、
dependsOn構造を下って伝播されます。commonMainに依存関係を追加すると、commonMainに対して直接的または間接的にdependsOn関係を宣言するすべてのソースセットに自動的に追加されます。この場合、依存関係は実際にすべての
*Mainソースセット(iosMain、jvmMain、iosSimulatorArm64Main、iosX64Main)に自動的に追加されました。これらのソースセットはすべてcommonMainソースセットからkotlin-coroutines-coreの依存関係を継承するため、手動ですべてにコピー&ペーストする必要はありません。伝播メカニズムにより、特定のソースセットを選択することで、宣言された依存関係を受け取るスコープを選択できます。例えば、
kotlinx.coroutinesをiOSで使用したいがAndroidでは使用したくない場合、この依存関係をiosMainにのみ追加できます。上記の
commonMainからorg.jetbrians.kotlinx:kotlinx-coroutines-core:1.7.3のような_ソースセット → マルチプラットフォームライブラリ_の依存関係は、依存関係解決の中間状態を表します。解決の最終状態は常に_ソースセット → ソースセット_の依存関係で表されます。最終的な_ソースセット → ソースセット_の依存関係は、
dependsOn関係ではありません。粒度の高い_ソースセット → ソースセット_の依存関係を推論するために、Kotlinは各マルチプラットフォームライブラリと共に公開されるソースセット構造を読み取ります。このステップの後、各ライブラリは全体としてではなく、そのソースセットのコレクションとして内部的に表現されます。
kotlinx-coroutines-coreの例を見てください。Kotlinは各依存関係を取り込み、それを依存関係からのソースセットのコレクションに解決します。そのコレクション内の各依存ソースセットは、_互換性のあるターゲット_を持っている必要があります。依存ソースセットが互換性のあるターゲットを持っているのは、コンシューマーソースセットと同じか、それ以上のターゲットにコンパイルされる場合です。
サンプルプロジェクトの
commonMainがandroidTarget、iosX64、およびiosSimulatorArm64にコンパイルされる例を考えてみましょう。- まず、
kotlinx-coroutines-core.commonMainへの依存関係を解決します。これは、kotlinx-coroutines-coreがすべての可能なKotlinターゲットにコンパイルされるためです。したがって、そのcommonMainは、必要なandroidTarget、iosX64、およびiosSimulatorArm64を含むすべての可能なターゲットにコンパイルされます。 - 次に、
commonMainはkotlinx-coroutines-core.concurrentMainに依存します。kotlinx-coroutines-coreのconcurrentMainはJSを除くすべてのターゲットにコンパイルされるため、コンシューマープロジェクトのcommonMainのターゲットに一致します。
しかし、コルーチンからの
iosX64Mainのようなソースセットは、コンシューマーのcommonMainとは互換性がありません。iosX64MainはcommonMainのターゲットの1つであるiosX64にコンパイルされますが、androidTargetにもiosSimulatorArm64にもコンパイルされないからです。依存関係解決の結果は、
kotlinx-coroutines-coreのどのコードが可視になるかに直接影響します。
- まず、
ソースセット間で共通の依存関係のバージョンを調整する
Kotlin Multiplatformプロジェクトでは、共通ソースセットは、klibを生成し、構成された各コンパイルの一部として、複数回コンパイルされます。一貫性のあるバイナリを生成するには、共通コードは毎回同じバージョンのマルチプラットフォーム依存関係に対してコンパイルされるべきです。Kotlin Gradleプラグインはこれらの依存関係を調整し、各ソースセットで実効的な依存関係のバージョンが同じであることを保証します。
上記の例で、androidMainソースセットにandroidx.navigation:navigation-compose:2.7.7依存関係を追加したいとします。あなたのプロジェクトはcommonMainソースセットに対してkotlinx-coroutines-core:1.7.3依存関係を明示的に宣言していますが、Compose Navigationライブラリのバージョン2.7.7はKotlinコルーチン1.8.0以降を必要とします。
commonMainとandroidMainは一緒にコンパイルされるため、Kotlin Gradleプラグインはコルーチンライブラリの2つのバージョンの中から選択し、commonMainソースセットにkotlinx-coroutines-core:1.8.0を適用します。しかし、共通コードがすべての設定されたターゲットで一貫してコンパイルされるように、iOSソースセットも同じ依存関係バージョンに制約される必要があります。そのため、Gradleはkotlinx.coroutines-*:1.8.0依存関係をiosMainソースセットにも伝播させます。
依存関係は、*Mainソースセットと*Testソースセットの間で別々に調整されます。*TestソースセットのGradle構成には*Mainソースセットのすべての依存関係が含まれますが、その逆はありません。これにより、メインコードに影響を与えることなく、新しいライブラリバージョンでプロジェクトをテストできます。
例えば、*MainソースセットにはKotlinコルーチン1.7.3の依存関係があり、それがプロジェクトのすべてのソースセットに伝播されているとします。しかし、iosTestソースセットでは、新しいライブラリリリースを試すためにバージョンを1.8.0にアップグレードすることにしました。同じアルゴリズムに従って、この依存関係は*Testソースセットのツリー全体に伝播されるため、すべての*Testソースセットはkotlinx.coroutines-*:1.8.0依存関係でコンパイルされます。
コンパイル
シングルプラットフォームプロジェクトとは異なり、Kotlin Multiplatformプロジェクトでは、すべてのアーティファクトをビルドするために複数回のコンパイラ起動を必要とします。各コンパイラの起動は_Kotlinコンパイル_です。
例えば、前述のKotlinコンパイル中にiPhoneデバイス用のバイナリがどのように生成されるかを見てみましょう。
Kotlinコンパイルはターゲットの下にグループ化されます。デフォルトでは、Kotlinは各ターゲットに対して2つのコンパイルを作成します。プロダクションソース用のmainコンパイルと、テストソース用のtestコンパイルです。
ビルドスクリプトでのコンパイルへのアクセスも同様の方法で行われます。まずKotlinターゲットを選択し、次にその内部のcompilationsコンテナにアクセスし、最後に名前で必要なコンパイルを選択します。
kotlin {
// Declare and configure the JVM target
jvm {
val mainCompilation: KotlinJvmCompilation = compilations.getByName("main")
}
}