Skip to content

Kotlin Multiplatformプロジェクト構造の基本

Kotlin Multiplatformを使用すると、異なるプラットフォーム間でコードを共有できます。この記事では、共有コードの制約、コードの共有部分とプラットフォーム固有の部分を区別する方法、およびこの共有コードが動作するプラットフォームを指定する方法について説明します。

また、共通コード、ターゲット、プラットフォーム固有および中間ソースセット、テスト統合など、Kotlin Multiplatformプロジェクト設定のコアコンセプトについても学習します。これにより、将来的にマルチプラットフォームプロジェクトをセットアップするのに役立ちます。

ここで提示されるモデルは、Kotlinが使用するモデルと比較して簡略化されています。しかし、この基本的なモデルはほとんどの場合で十分であるはずです。

共通コード

_共通コード_は、異なるプラットフォーム間で共有されるKotlinコードです。

単純な「Hello, World」の例を考えます。

kotlin
fun greeting() {
    println("Hello, Kotlin Multiplatform!")
}

プラットフォーム間で共有されるKotlinコードは通常、commonMainディレクトリに配置されます。コードファイルの場所は、このコードがコンパイルされるプラットフォームのリストに影響するため重要です。

Kotlinコンパイラはソースコードを入力として受け取り、その結果としてプラットフォーム固有のバイナリのセットを生成します。マルチプラットフォームプロジェクトをコンパイルする場合、同じコードから複数のバイナリを生成できます。たとえば、コンパイラは同じKotlinファイルからJVMの.classファイルとネイティブ実行可能ファイルを生成できます。

共通コード

すべてのKotlinコードがすべてのプラットフォームにコンパイルできるわけではありません。Kotlinコンパイラは、プラットフォーム固有の関数やクラスを共通コードで使用することを防ぎます。なぜなら、このコードは異なるプラットフォームにコンパイルできないためです。

たとえば、java.io.Fileの依存関係を共通コードから使用することはできません。これはJDKの一部ですが、共通コードはネイティブコードにもコンパイルされ、そこではJDKクラスは利用できません。

未解決のJava参照

共通コードでは、Kotlin Multiplatformライブラリを使用できます。これらのライブラリは、異なるプラットフォームで異なる方法で実装できる共通APIを提供します。この場合、プラットフォーム固有のAPIは追加部分として機能し、共通コードでそのようなAPIを使用しようとするとエラーになります。

たとえば、kotlinx.coroutinesはすべてのターゲットをサポートするKotlin Multiplatformライブラリですが、fun CoroutinesDispatcher.asExecutor(): Executorのようにkotlinx.coroutinesの並行プリミティブをJDKの並行プリミティブに変換するプラットフォーム固有の部分も持っています。このAPIの追加部分はcommonMainでは利用できません。

ターゲット

ターゲットは、Kotlinが共通コードをコンパイルするプラットフォームを定義します。これらは、たとえばJVM、JS、Android、iOS、またはLinuxである可能性があります。前の例では、共通コードをJVMとネイティブターゲットにコンパイルしました。

_Kotlinターゲット_は、コンパイルターゲットを記述する識別子です。それは、生成されるバイナリの形式、利用可能な言語構造、および許可される依存関係を定義します。

ターゲットはプラットフォームとも呼ばれます。サポートされているターゲットの完全なリストはこちらをご覧ください。

特定のターゲットのコードをコンパイルするようにKotlinに指示するには、まずターゲットを_宣言_する必要があります。Gradleでは、kotlin {}ブロック内で定義済みのDSL呼び出しを使用してターゲットを宣言します。

kotlin
kotlin {
    jvm() // Declares a JVM target
    iosArm64() // Declares a target that corresponds to 64-bit iPhones
}

このようにして、各マルチプラットフォームプロジェクトはサポートされるターゲットのセットを定義します。ビルドスクリプトでのターゲットの宣言について詳しく知るには、階層型プロジェクト構造セクションを参照してください。

jvmiosArm64ターゲットが宣言されている場合、commonMain内の共通コードはこれらのターゲットにコンパイルされます。

ターゲット

特定のターゲットにどのコードがコンパイルされるかを理解するために、ターゲットをKotlinソースファイルに付加されたラベルとして考えることができます。Kotlinはこれらのラベルを使用して、コードをコンパイルする方法、生成するバイナリ、およびそのコードで許可される言語構造と依存関係を決定します。

greeting.ktファイルを.jsにもコンパイルしたい場合は、JSターゲットを宣言するだけで済みます。その後、commonMain内のコードはJSターゲットに対応する追加のjsラベルを受け取り、Kotlinに.jsファイルを生成するように指示します。

ターゲットラベル

これが、Kotlinコンパイラが宣言されたすべてのターゲットにコンパイルされる共通コードで動作する方法です。プラットフォーム固有のコードの記述方法については、ソースセットを参照してください。

ソースセット

_Kotlinソースセット_は、独自のターゲット、依存関係、およびコンパイラオプションを持つソースファイルのセットです。これは、マルチプラットフォームプロジェクトでコードを共有する主要な方法です。

マルチプラットフォームプロジェクトの各ソースセットは次のとおりです。

  • 特定のプロジェクトで一意の名前を持ちます。
  • 通常、ソースセットの名前が付いたディレクトリに保存されているソースファイルとリソースのセットを含みます。
  • このソースセットのコードがコンパイルされるターゲットのセットを指定します。
  • これらのターゲットは、このソースセットで利用可能な言語構造と依存関係に影響を与えます。
  • 独自の依存関係とコンパイラオプションを定義します。

Kotlinには、多数の事前定義されたソースセットが用意されています。そのうちの1つはcommonMainで、すべてのマルチプラットフォームプロジェクトに存在し、宣言されたすべてのターゲットにコンパイルされます。

Kotlin Multiplatformプロジェクトでは、src内のディレクトリとしてソースセットを操作します。 たとえば、commonMainiosMainjvmMainのソースセットを持つプロジェクトは、次の構造を持ちます。

共有ソース

Gradleスクリプトでは、kotlin.sourceSets {}ブロック内で名前によってソースセットにアクセスします。

kotlin
kotlin {
    // Targets declaration:
    // …

    // Source set declaration:
    sourceSets {
        commonMain {
            // Configure the commonMain source set
        }
    }
}

commonMain以外に、他のソースセットはプラットフォーム固有または中間である場合があります。

プラットフォーム固有のソースセット

共通コードのみを持つことは便利ですが、常に可能であるとは限りません。commonMain内のコードは宣言されたすべてのターゲットにコンパイルされ、Kotlinはそこにプラットフォーム固有のAPIを使用することを許可しません。

ネイティブおよびJSターゲットを持つマルチプラットフォームプロジェクトでは、commonMain内の次のコードはコンパイルされません。

kotlin
// commonMain/kotlin/common.kt
// Doesn't compile in common code
fun greeting() {
    java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}

解決策として、Kotlinはプラットフォーム固有のソースセット(プラットフォームソースセットとも呼ばれる)を作成します。各ターゲットには、そのターゲットのみにコンパイルされる対応するプラットフォームソースセットがあります。たとえば、jvmターゲットには、JVMのみにコンパイルされる対応するjvmMainソースセットがあります。Kotlinは、これらのソースセットでプラットフォーム固有の依存関係を使用することを許可します。たとえば、jvmMainではJDKを使用できます。

kotlin
// jvmMain/kotlin/jvm.kt
// You can use Java dependencies in the `jvmMain` source set
fun jvmGreeting() {
    java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}

特定のターゲットへのコンパイル

特定のターゲットへのコンパイルは、複数のソースセットで機能します。Kotlinがマルチプラットフォームプロジェクトを特定のターゲットにコンパイルする場合、そのターゲットでラベル付けされたすべてのソースセットを収集し、それらからバイナリを生成します。

jvmiosArm64、およびjsターゲットの例を考えます。Kotlinは、共通コード用のcommonMainソースセットと、特定のターゲットに対応するjvmMainiosArm64Main、およびjsMainソースセットを作成します。

特定のターゲットへのコンパイル

JVMへのコンパイル中、Kotlinは「JVM」とラベル付けされたすべてのソースセット、すなわちjvmMaincommonMainを選択します。その後、それらをまとめてJVMクラスファイルにコンパイルします。

JVMへのコンパイル

KotlinはcommonMainjvmMainを一緒にコンパイルするため、結果のバイナリにはcommonMainjvmMainの両方からの宣言が含まれます。

マルチプラットフォームプロジェクトで作業する場合、次の点に注意してください。

  • Kotlinに特定のプラットフォームにコードをコンパイルさせたい場合は、対応するターゲットを宣言します。
  • コードを保存するディレクトリまたはソースファイルを選択するには、まずどのターゲット間でコードを共有するかを決定します。
    • コードがすべてのターゲット間で共有される場合、commonMainで宣言する必要があります。
    • コードが1つのターゲットのみに使用される場合、そのターゲットのプラットフォーム固有のソースセット(たとえば、JVMの場合はjvmMain)で定義する必要があります。
  • プラットフォーム固有のソースセットで記述されたコードは、共通ソースセットからの宣言にアクセスできます。たとえば、jvmMain内のコードはcommonMainからのコードを使用できます。しかし、その逆は真ではありません。commonMainjvmMainからのコードを使用できません。
  • プラットフォーム固有のソースセットで記述されたコードは、対応するプラットフォームの依存関係を使用できます。たとえば、jvmMain内のコードは、GuavaSpringのようなJava専用ライブラリを使用できます。

中間ソースセット

単純なマルチプラットフォームプロジェクトには、通常、共通コードとプラットフォーム固有のコードしかありません。commonMainソースセットは、宣言されたすべてのターゲット間で共有される共通コードを表します。jvmMainのようなプラットフォーム固有のソースセットは、それぞれのターゲットのみにコンパイルされるプラットフォーム固有のコードを表します。

実際には、より詳細なコード共有が必要になることがよくあります。

すべての最新のAppleデバイスとAndroidデバイスをターゲットにする必要がある例を考えます。

kotlin
kotlin {
    androidTarget()
    iosArm64()   // 64-bit iPhone devices
    macosArm64() // Modern Apple Silicon-based Macs
    watchosX64() // Modern 64-bit Apple Watch devices
    tvosArm64()  // Modern Apple TV devices  
}

そして、すべてのAppleデバイス用のUUIDを生成する関数を追加するためのソースセットが必要です。

kotlin
import platform.Foundation.NSUUID

fun randomUuidString(): String {
    // You want to access Apple-specific APIs
    return NSUUID().UUIDString()
}

この関数をcommonMainに追加することはできません。commonMainはAndroidを含む宣言されたすべてのターゲットにコンパイルされますが、platform.Foundation.NSUUIDはAndroidでは利用できないApple固有のAPIです。commonMainNSUUIDを参照しようとすると、Kotlinはエラーを表示します。

このコードを各Apple固有のソースセット、すなわちiosArm64MainmacosArm64MainwatchosX64Main、およびtvosArm64Mainにコピー&ペーストすることもできます。しかし、このようにコードを複製する方法はエラーの原因となりやすいため、推奨されません。

この問題を解決するには、_中間ソースセット_を使用できます。中間ソースセットは、プロジェクト内のすべてのターゲットではなく、一部のターゲットにコンパイルされるKotlinソースセットです。中間ソースセットは、階層型ソースセット、または単に階層と呼ばれることもあります。

Kotlinは、デフォルトでいくつかの中間ソースセットを作成します。この特定の場合、結果のプロジェクト構造は次のようになります。

中間ソースセット

ここで、下部の多色のブロックはプラットフォーム固有のソースセットです。分かりやすくするために、ターゲットラベルは省略されています。

appleMainブロックは、Apple固有のターゲットにコンパイルされるコードを共有するためにKotlinによって作成された中間ソースセットです。appleMainソースセットはAppleターゲットのみにコンパイルされます。したがって、KotlinはappleMainでApple固有のAPIを使用することを許可しており、randomUUID()関数をここに追加できます。

階層型プロジェクト構造を参照して、Kotlinがデフォルトで作成および設定するすべての中間ソースセットを見つけ、デフォルトで必要とする中間ソースセットがKotlinによって提供されない場合にどうすべきかを学びましょう。

特定のターゲットへのコンパイル中、Kotlinは、このターゲットでラベル付けされた中間ソースセットを含むすべてのソースセットを取得します。したがって、commonMainappleMain、およびiosArm64Mainソースセットに記述されたすべてのコードは、iosArm64プラットフォームターゲットへのコンパイル中に結合されます。

ネイティブ実行可能ファイル

一部のソースセットにソースがなくても問題ありません。たとえば、iOS開発では、通常、iOSデバイスに固有だがiOSシミュレーターには固有ではないコードを提供する必要はありません。したがって、iosArm64Mainはめったに使用されません。

Appleデバイスとシミュレーターのターゲット

Kotlin Multiplatformを使用してiOSモバイルアプリケーションを開発する場合、通常はiosMainソースセットを使用します。iosターゲットのプラットフォーム固有のソースセットだと考えるかもしれませんが、単一のiosターゲットは存在しません。ほとんどのモバイルプロジェクトには、少なくとも2つのターゲットが必要です。

  • デバイスターゲットは、iOSデバイスで実行できるバイナリを生成するために使用されます。現在、iOSのデバイスターゲットはiosArm64のみです。
  • シミュレーターターゲットは、お使いのマシンで起動されるiOSシミュレーター用のバイナリを生成するために使用されます。Apple silicon Macコンピューターをお持ちの場合は、iosSimulatorArm64をシミュレーターターゲットとして選択してください。IntelベースのMacコンピューターをお持ちの場合は、iosX64を使用してください。

iosArm64デバイスターゲットのみを宣言した場合、ローカルマシンでアプリケーションとテストを実行およびデバッグすることはできません。

iosArm64MainiosSimulatorArm64MainiosX64Mainのようなプラットフォーム固有のソースセットは、iOSデバイスとシミュレーター向けのKotlinコードが通常同じであるため、通常は空です。それらすべてでコードを共有するには、iosMain中間ソースセットのみを使用できます。

他のMac以外のAppleターゲットにも同じことが当てはまります。たとえば、Apple TV用のtvosArm64デバイスターゲットと、Apple siliconおよびIntelベースのデバイス上のApple TVシミュレーター用のtvosSimulatorArm64およびtvosX64シミュレーターターゲットがある場合、それらすべてにtvosMain中間ソースセットを使用できます。

テストとの統合

実際のプロジェクトでは、主要なプロダクションコードに加えてテストも必要です。これが、デフォルトで作成されるすべてのソースセットにMainTestのサフィックスが付いている理由です。Mainにはプロダクションコードが含まれ、Testにはこのコードのテストが含まれます。それらの間の接続は自動的に確立され、テストは追加の構成なしでMainコードによって提供されるAPIを使用できます。

Testに対応するものは、Mainと同様にソースセットです。たとえば、commonTestcommonMainの対応物であり、宣言されたすべてのターゲットにコンパイルされるため、共通テストを記述できます。jvmTestのようなプラットフォーム固有のテストソースセットは、プラットフォーム固有のテスト、たとえばJVM固有のテストやJVM APIを必要とするテストを記述するために使用されます。

共通テストを記述するためのソースセットがあるだけでなく、マルチプラットフォームテストフレームワークも必要です。Kotlinは、@kotlin.TestアノテーションとassertEqualsassertTrueなどのさまざまなアサーションメソッドが付属するデフォルトのkotlin.testライブラリを提供します。

各プラットフォームのプラットフォーム固有のテストを、それぞれのソースセットで通常のテストのように記述できます。メインコードと同様に、各ソースセットにプラットフォーム固有の依存関係を持つことができます。たとえば、JVMにはJUnit、iOSにはXCTestなどです。特定のターゲットのテストを実行するには、<targetName>Testタスクを使用します。

マルチプラットフォームテストの作成と実行方法については、マルチプラットフォームアプリのテストチュートリアルで学習してください。

次のステップ