Kotlin のマルチプラットフォームモバイル開発の話

# はじめに！

今年久しぶりに ACCESS Advent Calendar 2022 参加させていただきます！
こちらはACCESS Advent calendarの24日目の記事となります。
今回の記事のテーマは **Kotlin のマルチプラットフォームモバイル開発** を試してみた記事です。
書かれていたコードは全部サンプルプロジェクト作ったら生成されているので、興味がありましたら、是非参考のリンクを辿って、試してみてください。

# Kotlin Multiplatform Mobile (KMM) とは?

Kotlin Multiplatform Mobile (KMM) は Android と iOS を開発するための Kotlin の SDK です。
以下の図を見ると、ビジネスロジックは共通することができますが、View は各 OS の実装になります。
Shared Code は Kotlin で書きます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/c2c52264-6b83-6c42-c0ca-762480a8cf6e.png" alt="kmm" width="600"/>

Kotlin Multiplatform の survey によると、共通できるコードは以下のようです。
ロジックのところは共通できるが、UI のところを共通するのは難しいところがあると思われます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/b1d8ccfb-9b23-7a53-d2a6-3cddc2e26e35.png" alt="kmm" width="600"/>

Netflix や VMWare などがこの技術を利用しているらしいです。
現在 KMM はまだβ版ですが、近いうちに stable になるのを期待しています！

## メリット・デメリット

メリットに関して、他の multiplatform と同じように、同じコードの内容を各OSのために書く量が少なくなります。
特別な API を使うとかは話は別ですが、これを使うことで、同じロジックの場合、１回だけ書くことで済みます。
一番感じられているのは元々 Android エンジニアの場合、楽に学習することができると思います。
iOS は Swift を使う場合、短時間でなれることが可能かもしれません。

デメリットに関しては、この SDK はまだβ段階ですので、正式にリリースされた時に、API が変わったり、機能が使えなくなったりすることがあるので、migration が大変になる可能性はなくはないと思います。
またいろんなバグもまだありますので、気をつけてしながら使いましょう。

Flutter という multiplatform SDK もあります。
KMM のアッポロッチは Flutter と少し違います。
* Flutter はできるだけ native のコードから abstract 化で実装されていますが、KMM はその逆で、native コードと共に動いているのは目標のようです。
* Flutter で UI を作るために Flutter の widget とかを使うことができますが、KMM では上書いたようにビジネスロジックは共通させることができますが、UI のところは、各 OS の UI を用いて実装します。例えば Jetpack Compose とか、SwiftUI とかを使います。
  * デメリットは同じ UI なのに、各 OS のために書く必要があります。
  * しかし、これはメリットとしても考えられます。例えば、直接 native component を使っているので、変な動きになってしまう心配はありません。また新しい UI component をすぐ使うことができるとかです。

ですので、native アプリに近いものを実現したい場合は KMM の方が良いかなと思っています。

# 環境設定

**注意**：iOS の特別のコードを書いたり、Simulator や実機で実行するため、Mac は必要です。

macOS の場合、**kdoctor** を実行して、環境がすでに整ったか確認できます。
`brew install kdoctor` で kdoctor をインストールすることができます。
実行すると、環境によって、以下のような結果を得られます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/8e99c03d-52db-3361-c73d-9debeba5760b.png" alt="kmm" width="600"/>

`Your system is ready` と言われても、Android Studio の KMM plugin がまだインストールされていないのが検知できなかったので、もう一度環境を確認しましょう。
Kotlin の公式サイトによると、以下はインストールする必要があります。
* Android Studio
* XCode
* JDK
* Kotlin Multiplatform Mobile Plugin
* Kotlin Plugin

細かいところは触れませんが、基本的に最新の stable のツールを使ってもいいです。
最新で問題が発生した場合、少し古めのバージョンを使ってください。

私が使っているバージョンは以下になります。
```
Android Studio Chipmunk | 2021.2.1 Patch 1
Build #AI-212.5712.43.2112.8609683, built on May 19, 2022
Runtime version: 11.0.12+0-b1504.28-7817840 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
macOS 13.0.1

XCode Version 14.1 (14B47b)

java 18.0.1.1 2022-04-22
Java(TM) SE Runtime Environment (build 18.0.1.1+2-6)
Java HotSpot(TM) 64-Bit Server VM (build 18.0.1.1+2-6, mixed mode, sharing)

Kotlin 212-1.7.10-release-333-AS5457.46
Kotlin Multiplatform Mobile 0.3.4(212-1.7.11-357-IJ)-116
```

# プロジェクト作り

KMM プロジェクトを作成してみました。

[File] → [New] → [New Project...] から、Kotlin Multiplatform App を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/84d61ec0-b58a-cd83-29a8-c459e66ee0ac.png" alt="kmm" width="600"/>

Android API を選択する時に気になりますが、API 21 (Android 5.0) の選択もありますので、一番古いのを試してみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/e61303d0-5ec0-8dd8-91dd-c1b5a236f503.png" alt="kmm" width="600"/>

その後に以下のような画面が出てきました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/23ed3763-a32e-8369-b909-96b6d241f67f.png" alt="kmm" width="600"/>

Finish 押したら、Hello world プロジェクトは完成です！

## プロジェクト structure

KMM では Root Project という表現があります。Root Project は共有モジュールを持ち、Android と iOS アプリはサブプロジェクトになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/9b852f0e-d554-7823-fa7c-41e45496963a.png" alt="kmm" width="600"/>

このリンクができたのは [Gradle multi-project mechanism](https://docs.gradle.org/current/userguide/multi_project_builds.html) を使っているからです。

Root Project と別に、Android プロジェクトは自分のプロジェクトがあり、iOS も同様です。
デフォルトプロジェクトのファイルツリーは以下のような感じです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/82669148-5f2b-fd28-004f-85120ec9554a.png" alt="kmm" width="300"/>

各 OS の特別のコードの場合は HelloKMMAndroid と HelloKMMiOS で書くことになります。
両方の場合は Shared のところに書きます。これで実行して見ます。

## Dependency

Android 開発をやると、`build.gradle` に関わることがあると思います。
KMM でも似たようなものがありまして、`Shared` ディレクトリで `build.gradle.kts` というファイルがあります。
このファイルで共通の depenndency、android 用の dependency、iOS 用の dependecy を指定することができます。

プロジェクトを作成すると、デフォルトの `build.gradle.kts` はすでに準備されています。
KMM では各 Source set の dependency を指定することができます。
Source Set のお主な項目は
* common main
* android main
* iOS main

各アイテムも子ターゲットがあります。例えば以下のようです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/e0d5effb-f08f-3839-46d7-88970c4bf925.png" alt="kmm" width="600"/>

main は test というペアがあります。
実際にサンプルプロジェクトからデフォルトのものはあります。定義は以下のような定義になっています。

```
plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
}

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        version = "1.0"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../HelloKMMiOS/Podfile")
        framework {
            baseName = "shared"
        }
    }
    
    sourceSets {
        val commonMain by getting
        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }
        val androidMain by getting
        val androidTest by getting
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
        }
        val iosX64Test by getting
        val iosArm64Test by getting
        val iosSimulatorArm64Test by getting
        val iosTest by creating {
            dependsOn(commonTest)
            iosX64Test.dependsOn(this)
            iosArm64Test.dependsOn(this)
            iosSimulatorArm64Test.dependsOn(this)
        }
    }
}

android {
    namespace = "com.rh.hellokmm"
    compileSdk = 32
    defaultConfig {
        minSdk = 21
        targetSdk = 32
    }
}
```

SDK Dependencies に関して、対応しているか、していないかはありますので、以下にご参考頂ければと思います。
https://kotlinlang.org/docs/multiplatform-add-dependencies.html
https://kotlinlang.org/docs/multiplatform-mobile-android-dependencies.html
https://kotlinlang.org/docs/multiplatform-mobile-ios-dependencies.html

## コードの流れ

サンプルプロジェクトからの内容ですが、main ロジックは以下になります。
Shared → src → commonMain → kotlin → com.rh.hellokmm → Greeting.kt のところで greeting 関数を書いてあります。

```
package com.rh.hellokmm

class Greeting {
    private val platform: Platform = getPlatform() // こちらは各 [os]Main で実装されています。
    fun greeting(): String {
        return "Hello, ${platform.name}!"
    }
}
```

getPlatform は commonMain のところでは interface だけを準備し、関数の中身は各 [os]Main で実装されています。
commonMain は以下です。
```
interface Platform {
    val name: String
}

expect fun getPlatform(): Platform
```

Android は
```
class AndroidPlatform : Platform {
    override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
}

actual fun getPlatform(): Platform = AndroidPlatform()
```

iOS は
```
import platform.UIKit.UIDevice

class IOSPlatform: Platform {
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getPlatform(): Platform = IOSPlatform()
```


各OS のビューを実装しなければならないので、Android の MainActivity は以下のような実装です。(大事なところだけ書きました)
```
package com.rh.hellokmm.android
...
import com.rh.hellokmm.Greeting
...
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting(Greeting().greeting())
                }
            }
        }
    }
}

@Composable
fun Greeting(text: String) {
    Text(text = text)
}
...
```

上記のアウトプットは以下です
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/477a55c0-4b58-1c78-607c-f217ae6209db.png" alt="kmm" width="300"/>

iOS のビューは以下のような感じです。

```
import SwiftUI
import shared // こちらは大事です

struct ContentView: View {
	let greet = Greeting().greeting()

	var body: some View {
		Text(greet)
	}
}
...
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/9da6473c-28d0-3ee6-a220-efe4efc181ae.png" alt="kmm" width="300"/>



## 実行

Android はそのまま Android Studio で実行したらできるんですが、iOS の方はちょっと一手間かかります。
XCode がまだ開いたことがない場合はまず、xcode を先に開く必要があります。その後 Android Studio に戻って、iOS のアプリを実行します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318479/f6e947c1-c343-6d25-daf4-b217deb62606.png" alt="kmm" width="300"/>

実行されたら、Simulator が実行され、アプリが起動されます。1回実行したら、xcode が閉じたままでも問題ないです。
iOS の方を実行すると、アプリをビルドし、simulator で実行させるため、裏で以下のコマンドのような実行されています。

```
Command line invocation:
    /Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -workspace /Users/KotlinMultiplatform/HelloKMM/HelloKMMiOS/HelloKMMiOS.xcworkspace -scheme HelloKMMiOS -configuration Debug OBJROOT=/Users/KotlinMultiplatform/HelloKMM/build/ios SYMROOT=/Users/KotlinMultiplatform/HelloKMM/build/ios -sdk iphonesimulator -arch x86_64
```

# 感想

* 現在はデフォルトのプロジェクトを実行してみるまでですが、わからないところもまだ多く、もっと色々試したかったです。
* Android エンジニアの場合、iOS を対応したかったら、IDE も慣れているし、kotlin を既に使えば、KMM を使うと学習曲線が急勾配ではないと感じています。
* 他の multiplatform の SDK を比較することがまだできていないです。
* デバッグは気になります。Android は Android Studio でそのままデバッグすることができますが、iOS の方は一手間かかないと出来なさそうです。View Hierarchy を確認したい時とか。(調査必要)

# 参考

この記事は以下に参考で書きました。
https://kotlinlang.org/lp/mobile/
https://blog.jetbrains.com/kotlin/2022/10/kmm-beta/
https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html
https://developers.cyberagent.co.jp/blog/archives/37611/
https://kotlinlang.org/docs/multiplatform.html#code-sharing-between-platforms
https://davidserrano.io/kotlin-multiplatform-mobile-goes-beta-what-is-kmm-how-does-it-compare-with-flutter-first-steps-to-create-a-kmm-app