Skip to content

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

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

また、共通コード(Common code)、ターゲット(Targets)、プラットフォーム固有(Platform-specific)および中間(Intermediate)ソースセット、テストの統合など、Kotlin Multiplatformプロジェクトのセットアップに関するコアコンセプトについても学びます。これらは、将来マルチプラットフォームプロジェクトをセットアップする際に役立ちます。

ここで提示するモデルは、Kotlinで使用されているものと比較して簡略化されています。しかし、この基本モデルは大部分のケースにおいて十分なはずです。

共通コード

「共通コード(Common code)」は、異なるプラットフォーム間で共有される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 では利用できません。

ターゲット

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

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

ターゲットは「プラットフォーム」と呼ばれることもあります。サポートされているターゲットの全リストを参照してください。

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

kotlin
kotlin {
    jvm() // JVMターゲットを宣言
    iosArm64() // 64ビットiPhoneに対応するターゲットを宣言
}

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

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

ターゲット

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

プロジェクトにターゲットが1つしかない場合(例:JVM)、共通コードから適切な可視性を持つターゲット固有のシンボルにアクセスできます。 しかし、2つ目のターゲットを追加すると、ターゲット固有のシンボルは共通コードで利用できなくなります。 移行や他の中間的なプロジェクト状態では、この制限に注意してください。

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

ターゲットラベル

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

ソースセット

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

マルチプラットフォームプロジェクトの各ソースセットは:

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

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

Kotlin Multiplatformプロジェクトでは、 src 内のディレクトリとしてソースセットを扱います。 たとえば、 commonMainiosMainjvmMain ソースセットを持つプロジェクトは、以下の構造になります。

共有ソース

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

kotlin
kotlin {
    // ターゲットの宣言:
    // …

    // ソースセットの宣言:
    sourceSets {
        commonMain {
            // commonMainソースセットの設定
        }
    }
}

commonMain 以外にも、他のソースセットはプラットフォーム固有、または中間(Intermediate)のいずれかになります。

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

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

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

kotlin
// commonMain/kotlin/common.kt
// 共通コードではコンパイルされない
fun greeting() {
    java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}

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

kotlin
// jvmMain/kotlin/jvm.kt
// `jvmMain` ソースセットではJavaの依存関係を使用できる
fun jvmGreeting() {
    java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}

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

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

jvmiosArm64js ターゲットを持つ例を考えてみましょう。Kotlinは共通コード用に commonMain ソースセットを、特定のターゲット用に対応する jvmMainiosArm64MainjsMain ソースセットを作成します。

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

JVMへのコンパイル中、Kotlinは "JVM" というラベルが付いたすべてのソースセット、つまり jvmMaincommonMain を選択します。そして、それらを一緒にJVMクラスファイルへとコンパイルします。

JVMへのコンパイル

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

マルチプラットフォームプロジェクトを扱う際は、以下の点に注意してください。

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

中間ソースセット

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

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

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

kotlin
kotlin {
    android()
    iosArm64()   // 64ビットiPhoneデバイス
    macosArm64() // Appleシリコン搭載の最新Mac
    watchosArm64() // 最新の64ビットApple Watchデバイス
    tvosArm64()  // 最新のApple TVデバイス  
}

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

kotlin
import platform.Foundation.NSUUID

fun randomUuidString(): String {
    // Apple固有のAPIにアクセスしたい
    return NSUUID().UUIDString()
}

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

このコードをコピーして、Apple固有の各ソースセット(iosArm64MainmacosArm64MainwatchosArm64MaintvosArm64Main)に貼り付けることもできます。しかし、このようにコードを複製するとエラーが発生しやすくなるため、このアプローチは推奨されません。

この問題を解決するために、「中間ソースセット(Intermediate source sets)」を使用できます。中間ソースセットは、プロジェクト内のすべてではなく、一部のターゲットに対してコンパイルされるKotlinソースセットです。中間ソースセットは「階層的ソースセット(Hierarchical source sets)」または単に「階層(Hierarchies)」と呼ばれることもあります。

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

中間ソースセット

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

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

Kotlinがデフォルトで作成およびセットアップするすべての中間ソースセットを確認し、Kotlinがデフォルトで必要な中間ソースセットを提供しない場合に何をすべきかを学ぶには、階層的プロジェクト構造を参照してください。

特定のターゲットへのコンパイル中、Kotlinはそのターゲットのラベルが付いた、中間ソースセットを含むすべてのソースセットを取得します。したがって、 commonMainappleMainiosArm64Main ソースセットで書かれたすべてのコードは、 iosArm64 プラットフォームターゲットへのコンパイル中に結合されます。

ネイティブ実行ファイル

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

Appleデバイスおよびシミュレータターゲット

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

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

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

iosArm64MainiosSimulatorArm64Main などのプラットフォーム固有ソースセットは、通常空です。これは、iOSデバイスとシミュレータ用のKotlinコードは通常同じだからです。これらすべてでコードを共有するには、 iosMain 中間ソースセットのみを使用できます。

これはMac以外の他のAppleターゲットにも当てはまります。たとえば、Apple TV用のデバイスターゲット tvosArm64 と、Appleシリコンデバイス上のApple TVシミュレータ用のシミュレータターゲット tvosSimulatorArm64 がある場合、これらすべてに対して 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 タスクを使用します。

マルチプラットフォームテストの作成および実行方法については、マルチプラットフォームアプリのテストチュートリアルを参照してください。

次のステップ