Compose Multiplatformを触ってみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/ad1209ef-0318-1d8e-b1d5-d3e763db2f36.png)

## 挨拶

初めまして。
bravesoft株式会社でエンジニアリングマネージャーらしき事をしているおじさんです。

以前弊社技術blogにて[KMMについての記事](https://www.bravesoft.co.jp/blog/archives/13377)を書いていたので、今回はUIも共通化出来るようになった「Compose Multiplatform」について触ってみて所感を書いていこうと思います。

## 始め方

まずは[公式](https://www.jetbrains.com/ja-jp/lp/compose-multiplatform/)の[チュートリアル](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-getting-started.html)を進めてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/1a1579e5-b791-7af1-4d92-b9cd9bd6b1d7.png)

こんな感じ。

まずは[環境を整える](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-setup.html)から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/a9ac8d43-4cc9-d4ba-3833-2cd7be0457df.png)

1つずつやっていこうと思うけど、一回斜め読みした結果`kdoctor`コマンドで必要な環境が整っているのかをチェック出来るようなので、まずこれをやってみる。

```shell
> brew install kdoctor
...

> kdoctor
Environment diagnose (to see all details, use -v option):
[✓] Operation System
[✓] Java
[!] Android Studio
  ! Android Studio (AI-222.4459.24.2221.9862592)
    Location: /Applications/Android Studio/Android Studio Flamingo.app
    Bundled Java: openjdk 17.0.6 2023-01-17
    Kotlin Plugin: 222-1.8.0-release-AS3739.54
    Kotlin Multiplatform Mobile Plugin: not installed
    Install Kotlin Multiplatform Mobile plugin - https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile
  ! Android Studio (AI-223.8836.35.2231.11005911)
    Location: /Applications/Android Studio/Android Studio Giraffe.app
    Bundled Java: openjdk 17.0.6 2023-01-17
    Kotlin Plugin: 223-1.9.0-release-358-AS8836.35.2231.11005911
    Kotlin Multiplatform Mobile Plugin: not installed
    Install Kotlin Multiplatform Mobile plugin - https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile
[✖] Xcode
  ✖ Xcode requires to perform the First Launch tasks
    Launch Xcode and complete setup
[!] CocoaPods
  ! CocoaPods configuration is not required, but highly recommended for full-fledged development
  ✖ System ruby is currently used
    CocoaPods is not compatible with system ruby installation on Apple M1 computers.
    Please install ruby via Homebrew, rvm, rbenv or other tool and make it default
    Detailed information: https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1/66556339#66556339
  ✖ CocoaPods requires your terminal to be using UTF-8 encoding.
    Consider adding the following to shell profile
    export LC_ALL=en_US.UTF-8

Conclusion:
  ✖ KDoctor has diagnosed one or more problems while checking your environment.
    Please check the output for problem description and possible solutions.
```

なんか色々出た。

Android Studioは`kotlin-multiplatform-mobile`プラグインが入ってないよってことかな。
2つ入っているけどどっちも古いので最新のKoalaも入れた方がいいかも。

Xcodeは入ってるけど一回起動してねって感じ。
そういやインストールしたけど時間切れになって放置してたな・・・

CocoaPodsは必須じゃないけど本格的な開発するなら強く推奨との事だが、今でもCocoaPodsってよく使うのかな？ 弊社ではSwift Package Managerを推奨しているけど。

とりあえずCocoaPodsの警告は無視して良さそうなので他についてそれぞれ対処していく。
ここは本筋ではないので特に記載しませんが、最終的に以下のようになっていれば良いはず。

```shell
> kdoctor
Environment diagnose (to see all details, use -v option):
[✓] Operation System
[✓] Java
[✓] Android Studio
[✓] Xcode
[!] CocoaPods
  ! CocoaPods configuration is not required, but highly recommended for full-fledged development
  ✖ System ruby is currently used
    CocoaPods is not compatible with system ruby installation on Apple M1 computers.
    Please install ruby via Homebrew, rvm, rbenv or other tool and make it default
    Detailed information: https://stackoverflow.com/questions/64901180/how-to-run-cocoapods-on-apple-silicon-m1/66556339#66556339
  ✖ CocoaPods requires your terminal to be using UTF-8 encoding.
    Consider adding the following to shell profile
    export LC_ALL=en_US.UTF-8

Conclusion:
  ✓ Your operation system is ready for Kotlin Multiplatform Mobile Development!
```
※ 案件都合で古めのAndroid Studioも残しているけど、キレイに消すには各バージョンでプラグインのインストールが必要でした。
※ CocoaPodsは一旦無視。`←色々エラーに見舞われて最終的にはインストールしました。`

次のステップに進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/cf4ff7da-df9f-24e2-9e8c-41f117d44f87.png)

ちょっと気になったのは1つめの[Kotlin Multiplatform ウィザード](https://kmp.jetbrains.com/?_gl=1*i4tven*_gcl_au*MTEyODk5NzgwMy4xNzE3MTMyMjM0*_ga*MTY2NDk5NDI3MS4xNzE3MTMyMTI0*_ga_9J976DJZ68*MTcxODMyMDQzNi4zLjEuMTcxODMyMzIzMy4yNS4wLjA.&_ga=2.69296787.222723844.1718320436-1664994271.1717132124)ってやつ。

ここで設定に応じたプロジェクトテンプレートがダウンロード出来るらしいけど、Android Studioから作った場合とどっちがいいのかな。Android Studioのバージョンが上がったり構成に若干変更があったとしたらこっちも都度メンテされていくのか気になった。
まぁこれも今は本筋ではないので無視するとして、テンプレートギャラリーページを覗いてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/a31b9246-d248-2098-b14f-e3a04083ba75.png)

上3つはUIも共通化するパターンとそれぞれのネイティブUIを使うパターン、KMMの最小限テンプレかな？
Amperってなんだろ。一旦無視します。

ひとまずチュートリアルに戻って新規プロジェクトを作ってみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/aad8de7f-45dc-f64d-bcde-abd0155c5a2f.png)

ダウンロードしてzipファイルを解凍後、Android Studio Koalaで開く。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/6726281e-cf63-e401-086b-18d2104117ea.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/e3977eb7-cbb2-c7c5-f35d-bccea552e0d0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/20ba21ef-b723-569e-4624-b3ab09315af7.png)

初期状態ではiOSのビルドはまだ出来ないらしい。
`iosApp/iosApp.xcodeproj`をXcodeで開いて初期セットアップをしろという事で開いてみたところ、iOS Simulatorが１つもないので時間が掛かってしまったが、今回はダウンロードしてみてもiOSの実行構成が出てこなかったため手動で追加をしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/536a57a9-ca53-a6a6-563f-785521374f59.png)

この辺りを参考に…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/c5f90177-f85f-bbd5-4af3-e85b65eb5d90.png)

こんな感じで追加する。

何故かシミュレーターが検知されなかったがビルドしてみるとちゃんと起動した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/3231af6c-21f9-83de-5d67-fda8865d3737.png)

Androidのシミュレーターでもビルドしてみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/7e211782-2f45-7b1f-1c7d-5ae7c15d859f.png)

うん、ちゃんと動きました。

デスクトップとWEBアプリケーションも試してみます。

構成を追加して、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/678d3390-078a-bc4c-6e61-c5a648212266.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/040d8cb1-ff63-e583-b74c-31ffbb79361d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/b08226f6-300e-e403-2883-af1433efc770.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/0cca0b7d-a0ed-6161-d7bb-f70c9b54e853.png)

どちらもちゃんと動きました。

## サンプルを動かす

さて、ここから先はチュートリアル通りにやっても面白くないのでサンプルを１つ動かしてみます。
見た目がいい感じだったので以下を使わせていただきました。

https://github.com/MohamedRejeb/Pokedex

とりあえず起動してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/4c45ef3f-f0a9-b4d0-011e-e5dab815c0b6.png)

iOSは以下のエラーが出て動かせなかった。

```
Selected scheme "Pods-Pokedex" has invalid 'PRODUCT_TYPE'
```

直接 `.xcworkspace` から起動してみると起動出来ました。 `(cocoapodsインストール済み)`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/752d4d1a-ac43-5b2f-280c-594185378c1d.png)

コードを見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/0b4ab7c6-5c73-501d-383a-4f77ece40ec5.png)

気になるディレクトリは `android` `ios` `shared` 辺り。(`desktop`は今回無視します。)

`android`ディレクトリから見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/dd740a55-753d-e195-97da-107c82f14852.png)

ソースは `MainActivity` しかなく、リソースファイルも最小限しかない事が分かる。

リソースファイルの中身はこんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/a777f974-5862-b21e-55e4-d5ef088e75a9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/d74ea51d-73e3-a520-97b3-d26dc2a5f885.png)

`MainActivity`を見てみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/dbc11b04-bd15-b094-82f2-03529145cb33.png)

ざっくりとやっている事はDIを除けばルートコンポーネントを作って `ContentView` に渡しているだけ。
本体は `RootComponent` と `ContentView` にあるのでしょう。

次は `ios`ディレクトリ を見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/dea8cc24-3b51-f3d4-cd05-27a97d1272b2.png)

ここはXcodeプロジェクト置き場らしいですね。
`android`ディレクトリと対になる名前のディレクトリだけど用途は別っぽいです。
一旦放置します。

`shared`ディレクトリを見ていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/c031bba9-035b-a67e-6dbc-79ff751e34ea.png)

`〜Test`は一旦置いておいて、`〜Main`を見ていきます。

まずは `androidMain` から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/ffc02bbb-8f67-e0a4-9618-f21472a0860c.png)

`ContentView` がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/6fe23875-cd6d-bff2-2dba-57ca147d914b.png)

これは共通化出来なかったのでしょうか。

`PokedexDispatchers` の中身は以下こんな感じ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/2a2ea5e5-7507-3fa5-b002-04c5a76f47f2.png)

`HttpClientFactory`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/b707faf3-1ed9-4c12-1491-4b19e17007be.png)

`AndroidSqlDriverFactory`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/ada36894-d01c-3705-387f-c811807f1268.png)

特に説明は不要だと思うので次は `iosMain` を見ていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/aa9c43bb-371f-792f-b8cc-798e1740396a.png)

大体 `androidMain` の方にあったファイルですが `main.ios.kt` ってファイルもあるので見ておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/779b3bed-8c4f-6726-b13c-e8e140a1784a.png)

コードを見ると `android` ディレクトリの `MainActivity` と同等のコードになっているようです。
iOSの場合はここにあるんですね。
なんとなく `android` と合わせたいけどディレクトリ構成は弄れるのかなぁ…

まぁ他のコードも見ていきます。

`ContentView`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/7e328f3d-9aae-0f65-0ba0-e5dfbaeddc27.png)

シンプルですね。
`Android` の方はテーマを含めたガワの部分もここに入ってましたが、`iOS` では `main.ios.kt` に書かれていました。
ただこれは実装上の揺れだと思います。

`PokedexDispatchers`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/6e84420b-0814-52e7-c97b-cab1555cd922.png)

Android側のコードとほぼ同じ内容ですが、以下の部分が違います。

**Android**

```
    ...
    override val main: CoroutineDispatcher = Dispatchers.Main.immediate
    override val io: CoroutineDispatcher = Dispatchers.IO
    ...
```

**iOS**

```
    ...
    override val main: CoroutineDispatcher = Dispatchers.Main
    override val io: CoroutineDispatcher = Dispatchers.Default
    ...
```

`Dispatchers.Main.immediate` と `Dispatchers.Main`、
`Dispatchers.IO` と `Dispatchers.Default` が違います。

なぜ敢えて分けているのかは分かりませんでした。

`HttpClientFactory`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/1b86d6d8-23cb-58fe-af26-bfd9e346a2e9.png)

`Android` と違って `Darwin` で初期化しています。

`SqlDriverFactory`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/0ca54d21-749f-cf55-7834-2677a1b5d118.png)

`Android` と違って `NativeSqliteDriver` を返しています。

最後に `commonMain` を見てみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/a01e2137-6b81-09fc-4a45-14f3b834056e.png)

ここに実際の画面の大部分が収まっています。
殆ど共通化されていて `Android` と `iOS` の違いはごく僅かでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3796622/aa8e9273-201d-ee4f-512a-8d06d18e2b5f.png)

各プラットフォーム別で書かれている `ContentView` から呼び出されている `RootContent` がこちら。
Jetpack Composeで書かれていて、これに慣れている人はサクサク書いていける予感はあります。

大体分かってきたので今回はここまでとしてまとめに入ります。

## 個人的な所感

弊社ではあまり積極的にマルチプラットフォーム開発を採用していないのですが、もしこれから採用するとしたらやっぱりFlutterかなぁと思いました。
既にいくつか実績もありますし、Flutterも成熟してきてライブラリ含めたエコシステムも充実していて、開発体験をみてもホットリロードの有無は大きいです。

またCompose Multiplatformの方は今回触っている中でなかなか期待通りに動かない部分が結構ありました。
まっさらなMacで環境を作っていった時と違い、既にJavaやAndroid Studioがいくつか入っている中で環境構築していく過程で躓いたり、サンプルがスムーズに動かなかったりと環境で躓く事が多い印象です。

この辺りは普段からAndroidアプリをバリバリ開発しているエンジニアであればまた違う感想を抱くかもしれないので是非試してみて欲しいなと思います。

もっとこういう点がFlutterよりも優れているよ、というご意見があればコメントいただけると参考に致します。

---

bravesoftではiOSアプリやAndroidアプリの開発を行っています。 アプリ開発に興味がある方は是非、採用ページをご確認ください。

https://recruit.jobcan.jp/bravesoft/show/b001/386578

https://www.wantedly.com/companies/www-bravesoft-co-jp

https://www.bravesoft.co.jp/