【KMP入門】プロジェクトの作成からサンプルアプリの起動まで

# Kotlin Multiplatform (KMP) とは
**AndroidネイティブアプリとiOSネイティブアプリを同時に開発する**ためのフレームワークです[^1]。

本来はAndroidネイティブ用とiOSネイティブ用で別々のプロジェクトを作り、別々のコードを書き…とするところを、同じプロジェクトとしてまとめられ、一部のコードをKotlinで共通化できるようになります。

:::note warn
本記事は **Jetpack Compose** と**SwiftUI**の、少なくともどちらか一方で開発経験のある人を想定としています。
:::

# プロジェクトの作成
KMPで新規プロジェクトを作るには **Kotlin Multiplatform Wizard** を使用します。
https://kmp.jetbrains.com

![p2ui9dxdad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/30be431f-c4e9-7f20-8317-ac36e62477e7.png)

今回はiOSの「**Do not share UI (use only SwiftUI)**」を選択します。他の設定は初期状態のままで構いません。

「**Share UI (with Compose Multiplatform UI framework)**」の方はCMPと呼ばれるもので、簡潔に言うとKotlin版Flutterなのですが、こちらはまた別の記事でご説明します。

# Fleetのダウンロード
次に、KMP開発元の公式エディターである**Fleet**をダウンロードします。
https://www.jetbrains.com/ja-jp/fleet/

ダウンロードが完了したらFleetを起動し、「Open...」から先ほど作成したプロジェクトを開きます。

![dryniu9jxm.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/f3e89c02-dbd3-942b-c28c-3ea98b31c5fc.png)


# KMPプロジェクトの見方

![ucnad6c3fg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/d58241c0-88a0-4e54-10c5-eb4d92b04417.png)

ぐわーっとファイルとディレクトリーが並んでいて驚くと思いますが、重要なのは
* **composeApp** … **Androidアプリ用のコード**(Kotlin) + 設定ファイル
* **iosApp** … **iOSアプリ用のコード**(Swift) + 設定ファイル
* **shared** … **Android/iOSで共有するコード**(Kotlin) + 設定ファイル

の三つです。それ以外のファイルやディレクトリーは基本的に触らなくても開発できます。

# とりあえずアプリを起動してみる
**Kotlin Multiplatform Wizard** で作成したプロジェクトには簡単なサンプルコードが含まれています。

まずはアプリを動かしてみた方が分かりやすいと思うので、シミュレーターで起動してみましょう。

実行は画面右上にあるRunボタンから出来ます。

![ihgdbf2u7b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/e483f69a-1bfd-4817-8770-6d118a2a957f.png)

Runボタンを押すと実行するデバイスの選択画面が出ます。
もし緑の雷マークが付いている場合はFleetが準備中なのでしばらく待ちましょう。

![595xck5t57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/94ef8b9b-2652-51a8-38ec-28e319d5740d.png)

普段AndroidやiOSアプリを開発している方は最初から実行する機種名が出ていると思いますが、そうでない場合は「No devices available」という表示になるか、赤い🔴に!マークのアイコンが付きます。
また、iOSアプリとして実行できるのはmacのみです。WindowsやLinuxで開発している方はAndroidのみとなります。

:::note warn
Android、iOSアプリの開発環境の準備はPCを初期化しないと書けないので後日追記予定です
:::

実行に成功すると以下のようなアプリが立ち上がります。

![p246uarh3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/e025f13d-2f04-7ceb-3099-0a7e6f7d614d.png)

「**Click me!**」と書かれたボタンをタップするとAndroid、iOSそれぞれに合わせたアイコンが表示されます。

# コードを読んでみる
## shared

![txz2mmu9z2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/c7a5187e-c533-8dd4-4f4e-55ec3aacb744.png)

`shared`は前述した通り、Android、iOSで共有するコードが格納されています。
普段iOSアプリを開発している方は`commonMain/kotlin/org/example/project`などの深い階層に驚くと思いますが、Androidアプリ開発ではお馴染みの仕様です。

まずは`commonMain/kotlin/org/example/project/Greeting.kt`を読んでみます。
```kotlin:Greeting.kt
package org.example.project

class Greeting {
    private val platform = getPlatform()

    fun greet(): String {
        return "Hello, ${platform.name}!"
    }
}
```

`Greeting`というクラスが定義されていて、`greet()`メソッドでは`getPlatform().name`で得られた文字列を返しているのが分かります。
`getPlatform()`の定義は`Platform.kt`にあります。

```kotlin:Platform.kt
package org.example.project

interface Platform {
    val name: String
}

expect fun getPlatform(): Platform
```
ところが、`getPlatform()`はありますが実装がありません。

`getPlatform()`の実装は`androidMain`の`Platform.android.kt`、`iosMain`の`Platform.ios.kt`にそれぞれあります。

```kotlin:Platform.android.kt
package org.example.project

import android.os.Build

class AndroidPlatform : Platform {
    // OSのAPIレベルを取得
    override val name: String = "Android ${Build.VERSION.SDK_INT}"
}

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

```kotlin:Platform.ios.kt
package org.example.project

import platform.UIKit.UIDevice

class IOSPlatform : Platform {
    // OSの種類とバージョンを取得
    override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

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

Androidはもちろん、iOSもKotlinでネイティブ機能が呼び出せます。ただし画面に表示させたり、音を鳴らしたりする処理は共有コードとして書くことが出来ず、それぞれのネイティブコード（`androidApp`、`iosApp`内のコード）として書く必要があります。

このように、KMPの共有コードでは、一つのメソッドをそれぞれのプラットフォーム用に分けて実装することが出来ます。
もちろん、不要であれば全ての共有コードを`commonMain`内だけで完結することも可能です。

ちなみに`shared/build.gradle.kts`では、共有コードで使用する外部ライブラリーを追加できます。

## iosApp

![efm85pnm36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/680c8f4c-e990-4601-5755-d56092c89374.png)
.webp)

普段iOSアプリを開発している人にはお馴染みのファイルが並んでいます。

```swift:ContentView.swift
import SwiftUI
import Shared

struct ContentView: View {
    // SwiftUIで変数の更新を画面に反映させる、変数の宣言時に@Stateなどを付ける
    @State private var showContent = false
    var body: some View {
        VStack {
            Button("Click me!") {
                withAnimation {
                    showContent = !showContent
                }
            }

            if showContent {
                VStack(spacing: 16) {
                    // Swiftのツバメのアイコンを表示
                    Image(systemName: "swift")
                        .font(.system(size: 200))
                        .foregroundColor(.accentColor)
                    // KMPの共通コードを呼び出して画面に表示させる
                    Text("SwiftUI: \(Greeting().greet())")
                }
                .transition(.move(edge: .top).combined(with: .opacity))
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

二行目の`import Shared`で、KMPの共有コードをインポートしています。

## composeApp

![8s96ej9xcb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/1ae3954e-5093-66c8-09b7-4d34f4b4d6ba.png)

`src/androidMain`以下に、普段Androidアプリを開発している人にはお馴染みのファイルが並んでいます。
`src/commonMain`には Jetpack Compose に依存しないファイルを入れるようです。
サンプルでは`compose-multiplatform.xml`という名前で、アプリに表示させるComposeのアイコンデータ（↑の起動画面にある箱みたいなアイコン）が格納されています。

```kotlin:App.kt
package org.example.project

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.ui.tooling.preview.Preview

import kotlinproject.composeapp.generated.resources.Res
import kotlinproject.composeapp.generated.resources.compose_multiplatform

// @Composableを付けると変数の更新を画面に反映できるようになる
@Composable
@Preview
fun App() {
    MaterialTheme {
        // showContentが変化すると画面が更新される
        var showContent by remember { mutableStateOf(false) }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = { showContent = !showContent }) {
                Text("Click me!")
            }
            AnimatedVisibility(showContent) {
                // KMPの共通コードを呼び出して文字列を取得
                val greeting = remember { Greeting().greet() }
                Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
                    Image(painterResource(Res.drawable.compose_multiplatform), null)
                    Text("Compose: $greeting")
                }
            }
        }
    }
}
```

# おわりに
最初にKMPでプロジェクトを作成すると複雑そうに見えますが、実際はAndroid、iOS、共有部分の三つに分かれているだけだとご理解いただけたら幸いです。

次回は簡単なカウンターアプリを作ってみます。
https://qiita.com/hyshu/items/e943fded39ca1d50746b

# 参考文献
https://pleiades.io/help/fleet/getting-started.html

# 備考
本記事は以下の環境で検証しました

```
Fleet: 1.44.151
Android Studio: 2024.1 (AI-241.18034.62.2411.12071903)
Xcode 16.2
```

[^1]: 更にはデスクトップアプリ、Web、サーバーサイドKotlinも一つのプロジェクトでまとめられ、一部のコードをKotlinで共通化できます