Kotlin/Native Multiplatform プロジェクトで Android/iOS 向けの共通ライブラリを作る

Kotlin/Native の Multiplatform プロジェクト (MPP) を使って、Android/iOS 両方に対応したライブラリを作成します。

Kotlin/Native MPP ライブラリプロジェクトは、Android アプリと iOS アプリとでなるべく実装を共通化を目指して設計します。

本記事のサンプルプロジェクトは [irgaly/kotlin-multiplatform](https://github.com/irgaly/kotlin-multiplatform) へ設置しています。

公式のドキュメントは [Multiplatform Project: iOS and Android](https://kotlinlang.org/docs/tutorials/native/mpp-ios-android.html) が近いと思います。

Kotlin/Native を Android/iOS アプリ開発に導入していくモチベーションはこちらにまとめています。 → [Kotlin/Native を Android/iOS アプリ開発に導入しよう](https://qiita.com/irgaly/items/3e6ee5bc3f59302bb76a)

## Kotlin, Kotlin/Native のバージョン

本記事は以下のバージョン時点の情報です。

* Kotlin 1.3.0-rc-131
    * Kotlin/Native 0.9.3
* Gradle 4.10.2

Gradle のバージョンも、上記バージョン以降のものを使用します。

## 開発環境

記事執筆時点の環境です。

* macOS Mojave 10.14
* Android Studio 3.2.1
* Xcode 10.0 (10A255)

IntelliJ IDEA, AppCode でも開発は可能です。

## Kotlin/Native Multiplatform プロジェクト (MPP) とは

Kotlin/Native Multiplatform プロジェクト (MPP) は、複数の環境向けのアプリやライブラリを生成するための Kotlin/Native プロジェクトの構成です。

すべてのプラットフォーム向けの共通の Common コードと、プラットフォーム固有の機能へアクセス可能な Platform specific コードを含めることができ、一つの MPP から、それぞれのプラットフォーム向けのアプリやライブラリが生成されます。

## プロジェクトの構成

サンプルプロジェクトの構成は以下の通りです。

![project_tree.png](https://qiita-image-store.s3.amazonaws.com/0/17931/cc72019f-0ff1-699f-d22c-031483f56189.png)

| ディレクトリ | 内容 |
| --- | --- |
| kotlin-multiplatform | |
| ├ android | Android アプリプロジェクト (ライブラリ動作確認用アプリ) |
| ├ ios.swift | Xcode プロジェクト (ライブラリ動作確認用アプリ) |
| ├ library | Kotlin/Native Multiplatform プロジェクト |
| │   ├ build.gradle | Kotlin/Native Multiplatform ビルド構成 |
| │   ├ android.gradle | Android Library ビルド構成 |
| │   ├ src/commonMain | Kotlin/Native Common Kotlin sourceSet |
| │   ├ src/android | Android Library 向け Kotlin sourceSet |
| │   ├ src/iosMain | Kotlin/Native iOS Framework 向け Kotlin sourceSet |
| ├ build.gradle | 全体のプロジェクト設定 |
| ├ settings.gradle | プロジェクトの定義 |
| └ ... | |


`library` モジュールが Kotlin/Native MPP です。

ライブラリの動作確認用にそれぞれのアプリプロジェクトを設置しています。`android` モジュールは通常の Android アプリプロジェクトで、`ios.swift` ディレクトリは通常の Xcode プロジェクトです。

このプロジェクトは成果物として iOS 向けの `.framework` と、Android 向けの `.aar` を作成します。`.framework` は Kotlin/Native としてビルドされますが、`.aar` は Kotlin/Native ではなく Android Library としてビルドします。そのため、MPP ビルド設定として `library/build.gradle` の他に `library/android.gradle` の Android ライブラリ向けビルド構成が混ざって存在することになります。

## Common sourceSet について

すべてのプラットフォームで共通の Common sourceSet と、プラットフォームごとに個別に実装する Platform specific sourceSet があります。
それぞれのプラットフォーム向けのビルド時に以下の組み合わせでビルドされます。

| プラットフォーム | 使われる sourceSets の組み合わせ |
| --- | --- |
| Android | `src/commonMain` + `src/android` |
| iOS | `src/commonMain` + `src/iosMain` |

Common コードは純粋な Kotlin/Native コードだけを実装することができます。Platform specific コードはプラットフォームのすべての機能へアクセスできる実装となります。
`src/android` , `src/iosMain` のプラットフォーム固有実装は最低限となるようにして、なるべく多くの共通ロジックを `src/commonMain` へ実装できるように設計していくことになります。

## build.gradle

プロジェクトのすべての gradle モジュール全体に共通するビルド設定です。

```groovy
buildscript {
    ext {
        kotlin_version = '1.3.0-rc-131'
        kotlinx_coroutines_version = '0.22.5'
    }

    repositories {
        jcenter()
        google()
        maven { url "https://kotlin.bintray.com/kotlinx" }
        maven { url "https://dl.bintray.com/kotlin/kotlin-eap" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        jcenter()
        google()
        maven { url "https://kotlin.bintray.com/kotlinx" }
        maven { url "https://dl.bintray.com/kotlin/kotlin-eap" }
    }
}
```

Kotlin `1.3.0-rc-131` のために kotlin-eap リポジトリを追加しています。

Android Library モジュールや動作検証用の Android モジュールを含んでいるため `com.android.tools.build:gradle` も定義します。

## library/build.gradle

Kotlin/Native MPP モジュールのビルド設定です。

```groovy
apply plugin: 'kotlin-multiplatform'
apply plugin: 'maven-publish'

ext {
    android_version = '1.0.0'
    android_build = 1
    android_library_name = "multiplatform"
    ios_version = '1.0.0'
    ios_framework_name = 'KotlinMultiPlatform'
}

apply from: 'android.gradle'
```

成果物として Android 向け .aar と iOS 向け .framework を生成することになるため、それぞれのリリース情報を `ext` に定義し、必要な箇所から参照できるようにしています。

MPP 構成とするため、`kotlin-multiplatform` plugin を適用しています。

また、`library` モジュールを Android Library プロジェクトとしてもビルドできるようにするため、後述の `android.gradle` を読み込んでいます。

続いて、`kotlin` ディレクティブで MPP を構成します。

```groovy
kotlin {
    targets {
        fromPreset(presets.android, 'android')
        fromPreset(presets.iosArm64, 'iosArm64')
        fromPreset(presets.iosX64, 'iosX64')

        configure([iosArm64, iosX64]) {
            compilations.main {
                outputKinds += FRAMEWORK
            }
        }
    }
```

`targets` により、MPP の成果物を定義します。
必要な設定は presets として提供されていますので、用意されている presets を指定します。

presets の一覧がどこにあるか不明ですが、とりあえず [kotlinTargetPresetss.kt](https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinTargetPresets.kt) で presets が作られているようです。

fromPresets の第二引数は任意の target 名です。
configure により `iosArm64`, `iosX64` target の成果物として iOS framework の `FRAMEWORK` を指定します。

outputKinds の値は [GRADLE_PLUGIN.md](https://github.com/JetBrains/kotlin-native/blob/master/GRADLE_PLUGIN.md) に記載があります。

> EXECUTABLE - an executable file;
> KLIBRARY - a Kotlin/Native library (*.klib);
> FRAMEWORK - an Objective-C framework;
> DYNAMIC - shared native library;
> STATIC - static native library.

```groovy
    sourceSets {
        commonMain {
            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version"
            }
        }
        commonTest {
            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlin_version"
                implementation "org.jetbrains.kotlin:kotlin-test-common:$kotlin_version"
            }
        }
        androidMain {
            dependencies {
                implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
            }
        }
        iosMain {
        }
        iosArm64Main {
            dependsOn iosMain
        }
        iosX64Main {
            dependsOn iosMain
        }
    }
}
```

それぞれの sourceSets を設定します。
各 sourceSets は `dependencies` により独立した依存関係を定義できるようになっています。

`commonMain` 、`commonTest` sourceSets は Common sourceSets として必ず定義されています。
`library/src/commonMain` 、 `library/src/commonTest` ディレクトリがソースコードの読み込み対象として設定されています。それぞれ dependencies により、Common が依存するライブラリを指定します。

target として `android`、 `iosArm64`、 `iosX64` を定義したため、それぞれ `androidMain`、 `iosArm64Main`、 `iosX64Main` が sourceSets として定義されています。

`androidMain` の sourceSets は `library/src/androidMain` ディレクトリが設定されるはずですが、今回は Android Library として `library/src/android` ディレクトリを用意しているため、`library/src/androidMain` ディレクトリは使いません。

`iosArm64Main` と `iosX64Main` は独自に定義している `iosMain` の設定を引き継ぐように `dependsOn` を設定しています。
`iosMain` の sourceSets は `library/src/iosMain` ディレクトリを対象として定義されているため、arm64 と x64 とは共通のディレクトリを指定していることになります。

```groovy
afterEvaluate {
    [linkReleaseFrameworkIosArm64, linkDebugFrameworkIosArm64,
     linkReleaseFrameworkIosX64, linkDebugFrameworkIosX64].forEach { task ->
        task.doFirst {
            def target = compilation.target.disambiguationClassifier
            def buildType = compilation.buildTypes.find{ it.debuggable == debuggable }.name.toLowerCase()
            compilation.extraOpts '-o', new File(buildDir, "bin/$target/${compilation.name}/$buildType/framework/${ios_framework_name}.framework").absolutePath
        }
        task.doLast {
            compilation.extraOpts.clear()
        }
    }
...
}
```

iOS .frameworkの生成にあたり、framework の名称はデフォルトでは gradle project name の `library.framework` となってしまいます。
現時点では適切な名前を指定するオプションが存在しないため、ワークアラウンドとして `afterEvaluate` で framework 名称を書き換えるスクリプトを仕込んでいます。

## library/android.gradle

`library/android.gradle` は `library/build.gradle` から参照されているビルド設定です。
これは通常の Android Library プロジェクトとして記載します。

```groovy
apply plugin: 'com.android.library'

android {
    compileSdkVersion 28

    archivesBaseName = android_library_name
    version android_version
    group 'net.irgaly.kotlin'

...

    sourceSets.each {
        def root = "src/android/${it.name}"
        it.setRoot(root)
        it.java.srcDirs += "${root}/kotlin"
    }

...

}
```

`archivesBaseName` で生成されるのライブラリ名を設定しています。

`sourceSets` で、 `library/android/main` ディレクトリをプロジェクトリソースディレクトリとして設定し、 `library/android/main/kotlin` ディレクトリを Android Library の sourceSets として設定しています。

また、MPP 設定側で pretsets.android を設定したことにより、Android Library のビルド設定のうち、sourceSets が更新されます。つまり、Android Library 側で `library/android/main/kotlin` ディレクトリが設定され、 MPP 側で `library/commonMain/kotlin` ディレクトリが追加されます。これで、Android Library をビルドするだけで Common sourceSets のクラスも取り込まれるようになります。

## Common コードと Platform specific コードの関係

Common と Platform specific コードの依存関係は以下の通りです。

![Kotlin_Native MPP.png](https://qiita-image-store.s3.amazonaws.com/0/17931/a2a55b57-dfe0-88ed-29f0-1ef1aa32094d.png)

Common コードは、純粋な Kotlin (Kotlin/Native) 実装である必要があり、Platform specific コードや Platform の機能を参照することはできません。逆に、Platform specific コードは Common コードを参照することができます。

Common コードから Platform specific コードへアクセスしたいときは Kotlin/Native MPP の機能である `expect` と `actual` を使います。

実際のクラス実装を示して、 `expect` / `actual` 実装を確認していきます。

## Common クラスの実装

Android/iOS で共通の実装として `User` クラスを実装します。
完全にプラットフォーム非依存のロジックであれば Common 以下に kotlin ファイルを設置するだけです。

`src/commonMain/kotlin/net/irgaly/kotlin/multiplatform/library/model/User.kt`

```kotlin
package net.irgaly.kotlin.multiplatform.library.model

/// プラットフォーム非依存オブジェクト
class User (val name: String)
```

この `User` クラスは、どこからでも使うことができます。

## expect / actual を用いた Platform specific クラスの実装

Common コードから Platform specific コードへアクセスするために `expect` / `actual` キーワードを使用します。

`src/commonMain/kotlin/net/irgaly/kotlin/multiplatform/library/model/platform/Platform.kt`

```kotlin
package net.irgaly.kotlin.multiplatform.library.platform

expect object Platform {
    val model: String
}
```

`expect` キーワードを指定することで、Common コード側にはインタフェースだけが定義され、その実装が Platform specific コード側に存在することを宣言します。
`expect` は class や object に宣言することで、内包するプロパティやメソッドがすべて `expect` であるとみなします。

今回は `Platform object` のインタフェースだけを Common コード側に定義しています。

`src/android/main/kotlin/net/irgaly/kotlin/multiplatform/library/model/platform/Platform.kt`

```kotlin
package net.irgaly.kotlin.multiplatform.library.platform

import android.os.Build

actual object Platform {
    actual val model: String
        get() = Build.MODEL
}
```

Android 側の `Platform object` 実装です。`actual` を宣言することで、`expect` に対応する実装であることを示します。
`Platform` と `Platform.model` の実装を提供するので、それぞれに `actual` を宣言しています。

こちらは通常の Android 向け Kotlin であるため、`android.os.Build` へアクセスしています。

`src/iosMain/kotlin/net/irgaly/kotlin/multiplatform/library/model/platform/Platform.kt`

```kotlin
package net.irgaly.kotlin.multiplatform.library.platform

import platform.UIKit.*

actual object Platform {
    actual val model: String
        get() = UIDevice.currentDevice.model
}
```

iOS 側の `Platform object` 実装です。こちらも同様に `actual` を宣言して `Platform` を実装しています。

Platform specific 側の実装であるため、iOS API である `UIDevice` を利用することができます。

## Android 向け .aar をビルドする

```sh
% ./gradlew :library:assembleRelease
```

通常の Android Library としてビルドされます。
`library/build/outputs/aar/multiplatform-1.0.0.aar` が生成されており、これを通常の Android ライブラリとして利用可能です。

![android_output.png](https://qiita-image-store.s3.amazonaws.com/0/17931/066a1e0c-e0ac-ba58-2582-0a091c64d360.png)

## iOS 向け .framework をビルドする

iOS 向けに .framework を生成します。

```sh
% ./gradlew :library:linkIosArm64
または
% ./gradlew :library:linkIosX64
```

以下のように、`library/build/bin` 以下に `KotlinMultiPlatform.framework` が生成されます。

![ios_output.png](https://qiita-image-store.s3.amazonaws.com/0/17931/03f75a2b-f34e-0a3f-6eb6-932d484e6dd6.png)

arm64 版、x64 版が別の .framework に分かれてしまっていて扱いにくい点は残念ですが、ネイティブバイナリ状態の framework として iOS アプリに組み込めるものとなっています。

`library.framework` は、ワークアラウンドで無理矢理フレームワーク名を変更したために残ったゴミです。削除して構いません。

## ライブラリ検証用 Android アプリモジュールを設定する

`.aar` を組み込むことで Android アプリからライブラリを利用することが可能ですが、
ライブラリの開発中は `.aar` を生成せずに直接ライブラリを読み込むようにすると動作確認も簡単です。

検証アプリ向けのビルド設定である `android/build.gradle` は通常の Android モジュールですが、ライブラリを参照するために以下の設定を追加します。

```groovy
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
...
}

dependencies {
...
    implementation project(':library')
}
```

`project(':library')` の依存関係を追加することで Common コードや Android Library コードへのアクセスが可能です。

## ライブラリ検証用 iOS Xcode プロジェクトを設定する

iOS も、ライブラリ開発中に検証できる環境を用意します。
こちらは Xcode プロジェクトの Build Phase から gradle を呼び出し、.framework を生成できるようにします。

Swift で Xcode プロジェクトを新規作成し、Build Settings に以下の設定を追加します。

![xcode_buildsettings.png](https://qiita-image-store.s3.amazonaws.com/0/17931/984af4fe-c7a1-c437-185c-e34abe3057e1.png)

| Key | Value |
| --- | --- |
| KN_LIBRARY_BUILD_PATH/Debug/Any iOS Simulator SDK | `$SRCROOT/../library/build/bin/iosX64/main/debug/framework/KotlinMultiPlatform.framework` |
| KN_LIBRARY_BUILD_PATH/Debug/Any iOS SDK | `$SRCROOT/../library/build/bin/iosArm64/main/debug/framework/KotlinMultiPlatform.framework` |
| KN_LIBRARY_BUILD_PATH/Release/Any iOS Simulator SDK | `$SRCROOT/../library/build/bin/iosX64/main/release/framework/KotlinMultiPlatform.framework` |
| KN_LIBRARY_BUILD_PATH/Release/Any iOS SDK | `$SRCROOT/../library/build/bin/iosArm64/main/release/framework/KotlinMultiPlatform.framework` |
| KN_LIBRARY_BUILD_TASK/Debug/Any iOS Simulator SDK | `linkDebugFrameworkIosX64` |
| KN_LIBRARY_BUILD_TASK/Debug/Any iOS SDK | `linkDebugFrameworkIosArm64` |
| KN_LIBRARY_BUILD_TASK/Release/Any iOS Simulator SDK | `linkReleaseFrameworkIosX64` |
| KN_LIBRARY_BUILD_TASK/Release/Any iOS SDK | `linkReleaseFrameworkIosX64` |

Build Phases の設定を追加します。

![xcode_buildphases.png](https://qiita-image-store.s3.amazonaws.com/0/17931/baaefc64-e443-7f5a-decb-21147bf19212.png)

`Compile Sources` の前に `Build Kotlin Common Library` として `Run Script Phase` を追加します。

```sh
mkdir -p "$SRCROOT/build"
"$SRCROOT/../gradlew" -p "$SRCROOT/../library" "$KN_LIBRARY_BUILD_TASK"
rm -rf "$SRCROOT/build/KotlinMultiPlatform.framework"
cp -a "$KN_LIBRARY_BUILD_PATH" "$SRCROOT/build/KotlinMultiPlatform.framework"
```

gradle で Kotlin/Native MPP をビルドし、その成果物を `build` ディレクトリへコピーしています。
ライブラリのビルドが必要であればそのビルド時間がかかりますが、すでにビルドが完了していれば gradle 側でビルドはスキップされることになります。

`Build Kotlin Common Library` を追加したのち、一度 Xcode プロジェクトをビルドして `build/KotlinMultiPlatform.framework` が設置されることを確認します。

`Link Binary With Libraries` に `build/KotlinMultiPlatform.framework` を追加します。

`Copy Bundle Resources` のあとに `Copy Kotlin Common Library` として `Copy Files Phase` を追加します。
`Destination` を `Framework` に設定し、`build/KotlinMultiPlatform.framework` を追加します。

これで Swift の Xcode プロジェクトから .framework を参照できるようになりました。以下の Swift コードによりライブラリを使用します。Swift からは通常の Objective-C Framework として認識されています。

`ViewController.swift`

```swift
import UIKit
import KotlinMultiPlatform

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        let user = User(name: "user name")
        label.text = "\(Platform.init().model) / \(user.name)"
    }
}
```

ちなみに、Kotlin/Native MPP により生成された `KotlinMultiPlatform.framework/Headers/KotlinMultiPlatform.h` は以下のようになっています。

Kotlin のクラスの共通基底クラスとして `KotlinBase` が定義され、 `KMPUser` クラスが Swift 向けに `User` という名称でアクセス可能となっていることが分かります。

`Platform` は Kotlin 側ではシングルトンの object ですが、Swift 側からは `Platform.init()` でそのシングルトンインスタンスへのアクセスとなります。まだ Kotlin から Swift 向けのインタフェースへの変換は洗練されていない印象です。


```objc

#import <Foundation/Foundation.h>

@class KMPUser, KMPPlatform;

NS_ASSUME_NONNULL_BEGIN

@interface KotlinBase : NSObject
- (instancetype)init __attribute__((unavailable));
+ (instancetype)new __attribute__((unavailable));
+ (void)initialize __attribute__((objc_requires_super));
@end;

@interface KotlinBase (KotlinBaseCopying) <NSCopying>
@end;

__attribute__((objc_runtime_name("KotlinMutableSet")))
__attribute__((swift_name("KotlinMutableSet")))
@interface KMPMutableSet<ObjectType> : NSMutableSet<ObjectType>
@end;

__attribute__((objc_runtime_name("KotlinMutableDictionary")))
__attribute__((swift_name("KotlinMutableDictionary")))
@interface KMPMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType>
@end;

@interface NSError (NSErrorKotlinException)
@property (readonly) id _Nullable kotlinException;
@end;

...

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("User")))
@interface KMPUser : KotlinBase
- (instancetype)initWithName:(NSString *)name __attribute__((swift_name("init(name:)"))) __attribute__((objc_designated_initializer));
@property (readonly) NSString *name;
@end;

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Platform")))
@interface KMPPlatform : KotlinBase
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
+ (instancetype)platform __attribute__((swift_name("init()")));
@property (readonly) NSString *model;
@end;

NS_ASSUME_NONNULL_END
```

## まとめ

Kotlin/Native MPP の基本的な構成を説明しました。
Kotlin/Native MPP では Common コードと Platform specific コードとに分かれており、
`expect` / `actual` キーワードで Cmmon コードと Platform specific コードが結合されます。

理屈の上では Platform specific コードからはプラットフォームのすべての機能にアクセスできますが、マルチプラットフォームプロジェクトの強みを活かすには Platform Specific コードを最小限となるように設計することをおすすめします。