KotlinMultiplatformMobileを触ろう

[Ateam Finergy Inc.× Ateam CommerceTech Inc.× Ateam Wellness Inc. Advent Calendar 2022](https://qiita.com/advent-calendar/2022/finergy-commerce-tech-wellness)の7日目は株式会社エイチームウェルネス @engabesi が担当します。

# はじめに
KotlinMultiplatformMobile(以下KMM)、皆さんご存知でしょうか
AndroidとiOSで一部ロジックを共有して開発できる代物ですね
似たものだとReactNativeやFlutter等が有名ですが、KMMはKotlinで書けるのが個人的に嬉しいポイント
そんなKMMを本当にさらっとだけですが触ったので所感を以下に記載していきます

# セットアップ
https://kotlinlang.org/docs/multiplatform-mobile-getting-started.html
これの流れに沿っていきます
細かい点は↑にあるのでざっくりとだけ記載します

Android Studioを起動します

`Plugins`から`Kotlin Multiplatform Mobile`をInstall

`New Project`からTemplateにある`Kotlin Multiplatform App`を選択

アプリ名等を決めてポチポチ押してProject作成します

androidAppとiosApp両方ビルドできるかを確認します
これだけでKMMのProject作成完了です
|android|ios|
|--|--|
|![Screenshot_1670099252.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/7a9cb19a-3211-50ad-220f-318995a20b5c.png)|![Simulator Screen Shot - iPhone 14 Pro - 2022-12-04 at 05.27.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/47774bf6-d16d-a965-334e-c8f304329f4b.png)|

サンプル時点で既に軽い共通処理、OSで分ける処理、interfaceの書き方が既に実装されています

~~正直これを見ただけで満足してしまいましたが~~、
ここにネイティブ機能を利用する場合の実装も追加していきます

ボタンをタップしたらOSの通知を飛ばす機能を作成します

# 通知実装

通知で表示する文字列は共通なので`shared/commonMain`に記載します

```kotlin:Greeting.kt
class Greeting {
    private val platform: Platform = getPlatform()
    val notificationTitle = "Hi!"

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

ボタンと通知処理を`androidApp/*`と`iosApp/*`配下に追記します

```kotlin:MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "CHANNEL_ID",
                "CHANNEL_NAME",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            val notificationManager: NotificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }

        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Button(onClick = {
                        val builder = NotificationCompat.Builder(this, "CHANNEL_ID")
                            .setSmallIcon(androidx.core.ktx.R.drawable.notification_icon_background)
                            .setContentTitle(Greeting().notificationTitle)
                            .setContentText(Greeting().greet())
                            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                        with(NotificationManagerCompat.from(this)) {
                            notify(1, builder.build())
                        }

                    }) {
                        Text(text = "Button")
                    }
                }
            }
        }
    }
}

```

```swift:ContentView.swift
struct ContentView: View {
    init(){
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: .alert) { _, _ in }
    }

	var body: some View {
        Button("Button") {
            sendNotification(title: Greeting().notificationTitle, body: Greeting().greet())
        }
	}
}

func sendNotification(title: String, body: String){
    let content = UNMutableNotificationContent()
    content.title = title
    content.body = body
    
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
    let request = UNNotificationRequest(
        identifier: "NOTIFY_1", content: content, trigger: trigger
    )
    UNUserNotificationCenter.current().add(request)
}
```
(iOS側は、これだとアプリがforegroundだと通知が表示されないですが今回の本題はそこではないのでそのままにします)

これだけで実装完了です

|android|ios|
|--|--|
|![Screenshot_1670104635.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/9aa889ac-dd0c-35cc-67d4-8f868f5452f5.png)|![Simulator Screen Shot - iPhone 14 Pro - 2022-12-04 at 06.57.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469399/154c2dba-a45a-1260-09a3-23086e1f190e.png)|

`shared/*`にロジックを記載し、UIとネイティブを利用する部分は`androidApp/*|iosApp/*`に記載していく形になります

通知の文字を変える際は`shared/commonMain`の文字列を変えるだけで両OSに適用されるので、
片方だけ変更が忘れられて仕様が異なる等の悲しい状態が少なくなりそうです

# 感想
処理が共通化されることにより意図しないOSによる差異を防げたり、
必然的にロジックとUIが分離されるためテストやチームでの分担が容易になりそうです

一方で、iOS側の実装はXcodeで行いましたが、
shared内の変更をUI側で使う際に一度ビルドするまでサジェストが行われなかったのはちょっと面倒に感じました
あとどうせならAndroidStudio上で`iosMain`も実装できるようになると更に捗りそうです
~~JetBrainsにはAppCodeがあるので全然その未来はありそうです~~
(追記)2022-12-14にAppCodeサポート終了してしまった…

少し触っただけなので実はできる事だった場合はごめんなさい

プロダクトで早く使ってみたいのでKMMからベータが取れる日を楽しみに待ちたいと思います