Compose Multiplatformを触ってみた

## この記事の目的

この記事では、Compose Multiplatformを始められる環境を整えることを目的にしています。
仕組み的なお話や、どうやって実現されているのか、という話は一旦置いておいて、Compose Multiplatform が何者なのかを理解して、環境を準備して触り始められるまでをゴールとしたいと思います。

## Compose Multiplatform とは
端的にいえば、「iOS, Android, Desktopアプリ開発で共通的に使えるKotlinによる宣言的UIフレームワーク」です。
あくまでUIフレームワークなので、Compose Multiplatform だけではロジックの共通化まではできません。ロジックの共通化に関しては Kotlin Multiplatform（KMP）、Kotlin Multiplatform Mobile（KMM）の責務になってきます。

KMPやKMMの解説については、こちらの記事の説明が分かりやすかったのでぜひ見てみてください。

https://developers.cyberagent.co.jp/blog/archives/37611/

これまで、技術選定としてKMMやKMPを検討する上で一番のネックだったことが、UIは各プラットフォームで別々に実装しないといけない、という点でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/25fae975-87ea-1678-eaa2-8c633a8d9765.png)
https://kotlinlang.org/lp/multiplatform/

すでに各プラットフォームで開発がある程度進んでいる状況で、保守が大変なので一部分だけでもコードを共通化したい、というようなケースではKMMやKMPは有力な候補です。
しかし、新規に立ち上げる少人数なプロジェクトで「ロジックはKotlin、UIはネイティブ」というのはリソース的にかなり辛く、私の場合は最終的に Electron や React Native、Flutter などを選定するケースが多かったです。

Compose Multiplatform がUI層の共通化を担ってくれることで、アプリケーションの全てのコードを Kotlin 化できる、というのが最大のうまみです。
この度、Compose Multiplatform for iOS が Experimental から Alpha になったということもあり、マルチプラットフォームなアプリを開発する際、（Android/Kotlin開発経験者が多ければ）有用なフレームワークになりうると思ったので、とにかく触ってみることにします。

## セットアップ

手元の環境は下記。Android、iOSの開発環境が整備されていることを前提としています。
* macOS Ventura 13.3.1
* Android Studio Flamingo 2022.2.1 Patch 2
* Xcode 14.3

Compose Multiplatform を試すには、JetBrainsが用意してくれているGitHubリポジトリのテンプレートを利用するのが一番手っ取り早いと思います。
2023/06/01時点では、下記4種類のテンプレートが用意されていました。

- https://github.com/JetBrains/compose-multiplatform-template
    - Desktop + iOS + Android のテンプレート
- https://github.com/JetBrains/compose-multiplatform-ios-android-template
    - iOS + Android のテンプレート
- https://github.com/JetBrains/compose-multiplatform-html-library-template
    - Web のテンプレート
- https://github.com/JetBrains/compose-multiplatform-desktop-template
    - Desktop のテンプレート

今回は Desktop + iOS + Android のテンプレート を選択しました。

テンプレートの[README](https://github.com/JetBrains/compose-multiplatform-template#check-your-environment)にも記載があるのですが、次は[KDoctor](https://github.com/Kotlin/kdoctor)というツールを使って開発環境のチェックを行います。

```shell
$ brew install kdoctor
$ kdoctor
```

`kdoctor`を実行すると、下記のようにKMMの開発環境が整っている旨のメッセージが表示されました。不足している設定があればここで表示されるとのこと。

```shell
Environment diagnose (to see all details, use -v option):
[✓] Operation System
[✓] Java
[✓] Android Studio
[✓] Xcode
[✓] Cocoapods

Conclusion:
  ✓ Your system is ready for Kotlin Multiplatform Mobile Development!
```

ここまででセットアップは完了です。

## 触ってみる

テンプレートから作成した自分のリポジトリをクローンし、Android Studio で開きます。
すると、自分の環境だとこんなエラーが発生しました。
```
A problem occurred configuring root project 'MyApplication'.
> Could not resolve all files for configuration ':classpath'.
   > Could not resolve com.android.tools.build:gradle:7.4.2.
```

このエラーは Gradle JDK をバージョン17に変更して再度 Sync Gradle で解消できました。今回 Android Studio を新しくインストールされた方なら発生しなさそうです。
※[似たような轍を踏んでいた](https://qiita.com/y-mimura/items/c14cab0f5769d21ae5c1)のですぐ気づけました。Gradle JDK の変更方法が分からない方はそちらの轍をご参照ください。

まずは各プラットフォームで実行してみます。
とはいえ、Android/Desktop 用にはすでにConfigurationが構成されていて、Configurationを選択するだけで実行できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/a0c45ada-05a8-36b0-472e-01594dd429f4.png)
iOSでの実行には少し追加設定が必要です。
Android Studio > Settings... > Plugins で Marketplace から `Kotlin Multiplatform Mobile` を検索してインストール。その後、Android Studio の再起動を促されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/e7ec286f-4c4f-0c40-e77c-71f1ed64af7d.png)
すると、Configurations に `iosApp` が追加されます。Simulatorの機種を変えたい場合、実機で実行したい場合などはテンプレートの[README](https://github.com/JetBrains/compose-multiplatform-template#check-your-environment)を参照してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/50b927fe-d5ec-55c9-c0bf-0e589a57b401.png)
ここまででAndroid, iOS, Desktop アプリのビルドができるようになってるはずです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238072/3abfcb7f-2406-1679-0324-9338c3da6e56.png)

コードを見てみると、`shared/`下の各プラットフォームの`xxxMain/kotlin/main.xxx.kt`で、`App()`という関数を呼び出しています。

```kotlin:shared/src/androidMain/kotlin/main.android.kt
import androidx.compose.runtime.Composable

actual fun getPlatformName(): String = "Android"

@Composable fun MainView() = App()
```

```kotlin:shared/src/iosMain/kotlin/main.ios.kt
import androidx.compose.ui.window.ComposeUIViewController

actual fun getPlatformName(): String = "iOS"

fun MainViewController() = ComposeUIViewController { App() }
```

```kotlin:shared/src/desktopMain/kotlin/main.desktop.kt
import androidx.compose.runtime.Composable

actual fun getPlatformName(): String = "Desktop"

@Composable fun MainView() = App()
```

`App()`の実体は`shared/src/commonMain`にありました。ここを編集すれば全てのプラットフォームに反映されます。

```kotlin:shared/src/commonMain/kotlin/App.kt
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.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.painterResource

@OptIn(ExperimentalResourceApi::class)
@Composable
fun App() {
    MaterialTheme {
        var greetingText by remember { mutableStateOf("Hello, World!") }
        var showImage by remember { mutableStateOf(false) }
        Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
            Button(onClick = {
                greetingText = "Hello, ${getPlatformName()}"
                showImage = !showImage
            }) {
                Text(greetingText)
            }
            AnimatedVisibility(showImage) {
                Image(
                    painterResource("compose-multiplatform.xml"),
                    null
                )
            }
        }
    }
}

expect fun getPlatformName(): String
```

## Next step...

公式リポジトリに、サンプルがたくさん用意されています。
次回は、サンプルを参考にしながら簡単なアプリケーションを作れたらいいなと思っています。

https://github.com/JetBrains/compose-multiplatform/blob/master/examples/README.md

## Q&A
社内でこの記事の内容を紹介した際に、いくつか質問が挙がったのでこの章で補足します。

**Q1. Desktop + iOS + Android + Web 全部入りのテンプレートはないの？**

**A1. 現時点ではありません。**
今のところ、Webとそれ以外のプラットフォームでのUIコンポーネントの仕組みが大きく異なるようで、Webとその他のプラットフォームをまとめたい！というケースには対応できないのかな、と推測しています。
Web単体のテンプレートを見てみて頂けるとわかるのですが、`Div`というコンポーネントが利用されています。

https://github.com/JetBrains/compose-multiplatform-html-library-template/blob/main/src/main/kotlin/main.kt
```kotlin:compose-multiplatform-html-library-template/blob/main/src/main/kotlin/main.kt
// import文等省略

@Composable
fun Body() {
    var counter by remember { mutableStateOf(0) }
    Div {
        Text("Clicked: ${counter}")
    }
    Button(
        attrs = {
            onClick { _ ->
                counter++
            }
        }
    ) {
        Text("Click")
    }
}
```
ただ、鋭意開発中のフレームワークですので、今後Webのコードも共通化できる可能性は大いにあると思います！お祈りして待ちましょう。

**Q2. Hot reload / Hot reflesh はできる？**

**A2. 現時点ではできなさそうです。**
これはテンプレートの[README](https://github.com/JetBrains/compose-multiplatform-template#make-your-first-changes)に記載がありました。今のところ、変更を反映させるためには Re-run する必要があるようです。

> 2.Update the shared code by adding a text field that will update the name displayed on the button:
> 3.Re-run the desktopApp, androidApp, and iosApp configurations. You'll see this change reflected in all three apps:

こっちは JetBrains が対応してくれなくても、世界のどこかの天才デベロッパーがプラグイン的なものを作ってくれないかな...と期待しています。

## 参考にさせていただいたサイト
https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/

https://github.com/JetBrains/compose-multiplatform

https://developers.cyberagent.co.jp/blog/archives/37611/

https://qiita.com/FujiKinaga/items/46431876d9c0f467fcf5#androidstudio%E4%B8%8A%E3%81%A7ios%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8Bplugin