KMP + Compose MultiplatformでFull Kotlinなモバイルアプリを作ってみた

この記事は、CYBOZU SUMMER BLOG FES '24 (Newcomer Stage) DAY 4の記事です。

こんにちは!AndroidアプリエンジニアのAtriaです。
24新卒としてサイボウズ株式会社に入社いたしました。

今年の技術職向けの新卒研修では実践研修というものがありました。実践研修では、新卒メンバー5人で1チームとなって2週間で何かを作るというハッカソン的なことが行われました。
そこで私は「せっかくだし触ったことない技術を触ってみよう!」ということでKotlin Multiplatform (KMP)とCompose Multiplatformを利用してみました。

この記事では、実際にKMP+Compose Multiplatformを使ってみた結果とその感想についてお話しさせていただきます。

KMPとは

Kotlin Multiplatform (KMP)は、Kotlinでのクロスプラットフォーム開発を可能にするフレームワークです。
Kotlinによって記述したコードを共有することで、ロジックを異なるプラットフォームで使い回すことができます。この時すべてのコードをKotlinで記述する必要はなく、一部だけKotlinを利用するということも可能です。
さらに、KMPに加えて後述のCompose Multiplatformを利用することで、ロジックだけでなくUI部分もKotlinコードで共有することが可能となります。

Compose Multiplatformとは

Compose Multiplatform は Kotlin と Jetpack Compose をベースとする Android、iOS、ウェブ、およびデスクトップ(JVM を使用)間で UI を共有するための宣言型UIフレームワークです。
Jetpack Composeの記法でUI共通化ができるため、とても便利です。
クロスプラットフォーム開発向け Kotlin Multiplatform | JetBrains

今回はロジック部分をKMP, UI部分をCompose Multiplatformで記述することにより、Full Kotlinなモバイルアプリケーション開発にチャレンジしてみました。

作ったもの

曖昧な記憶やキーワードからkintoneの閲覧履歴を検索するサービス「tekitone(テキトーン)」をサイボウズ社内向けに開発しました。
※このサービスを外部公開する予定はございません。

技術選定のモチベーション

今回の技術選定のモチベーションは以下のとおりです。

  • 自分一人でモバイルを担当する必要がある
  • 新しい技術を勉強したい
  • 多くの方に利用してもらいたい、より多くのスマートフォンデバイスで動かしたい
  • スピード感を持って開発したい、ハッカソンなので持続性はとりあえず考えない

今回のハッカソンでは、ネイティブによるモバイルアプリ開発を担当できるのは私だけでした。私はAndroidアプリエンジニアで、iOSアプリ開発の経験はほとんどありません。
そんな状況の中でも、tekitoneをできるだけ多くの方に利用してもらいたいというモチベーションからAndroid/iOSの両方でアプリを動かしたいと考えました。
また、今回のハッカソンは2週間という限られた時間で開発する必要があるため、今からiOSを勉強するなどしてそれぞれネイティブで開発するよりもクロスプラットフォームフレームワークを利用する方が速度が出ると判断しました。

クロスプラットフォームフレームワークは初め、FlutterReact Native.NET MAUIが思い浮かびました。
しかし先日 Kotlin Fest 2024 に参加していた私は脳内がずっと「KMPやりたい!」となっていたため、今回はKMP+Compose Multiplatformを採用しました。

Kotlin Fest 2024 の参加レポはこちら!(オレンジの服を羽織っている私が写っています)
Kotlin Fest 2024 にスポンサーとして参加しました! - Cybozu Inside Out | サイボウズエンジニアのブログ

実際の画面

実際にtekitoneをiOS/Androidデバイスで動かしている画面がこちらです。

ログイン画面とメイン画面

Compose MultiplatformによってiOSとAndroidそれぞれでUIの共通化を実現しています。
例えば、ログイン画面のUI部分は以下のようなコードで表現できます。

@Composable
fun LoginScreen(
    viewModel: LoginScreenViewModel,
    navigateToMainScreen: () -> Unit = {},
) {
    val uiState by viewModel.uiState.collectAsState()
    if (uiState.alreadyLoggedIn) {
        navigateToMainScreen()
    }
    
    if (uiState.isLoading) {
        TekitoneCircularProgressIndicator()
    } else {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxWidth().fillMaxHeight(),
        ) {
            Spacer(Modifier.weight(1f))
            Image(
                painter = painterResource(Res.drawable.logo),
                contentDescription = "tekitone-logo",
            )
            Spacer(Modifier.height(40.dp))
            TekitoneButton(
                onClick = viewModel::invokeLogin,
                modifier = Modifier.width(200.dp).height(40.dp),
            ) {
                Text("ログイン")
            }
            Spacer(Modifier.weight(1f))
        }
    }
}

Compose MultiplatformではいわゆるViewModelやuiStateなどの機能も利用できるため、普段の開発体験と大差なくUIを表現することができます。
このコードでAndroid端末だけでなく、iOSのUIも表現できるって便利じゃないですか?

動いている様子は用意がなくこの場では見せることができませんが、画面遷移や検索などのロジック部分はKMPによって共通化が行われています。

利用したIDEについて

Android Studio

安心と信頼のAndroid Studioです。
KMP ProjectでもAndroid Studioは利用可能です。

動作も快適で、問題なく開発することができます。

JetBrains Fleet

JetBrains FleetはKotlin Multiplatformをサポートしている新しいIDEです。
現在はプレビュー版となっており、プレビュー版の間は無料で提供されるようです。

JetBrains Fleet を利用する利点として、KMP ProjectでのPreview機能があります。
Android Studioではできない「common main」内でのPreviewコードのプレビューがJetBrains Fleetでは可能です。
JetBrains Fleet: コードエディターを超えるツール

両方使ってみた結果

最初はJetBrains Fleetで書いていたのですが、以下の弱点を感じました。

  • コード解析に時間がかかるため、補完が効かない時間が気になる
  • Previewの更新に若干時間がかかる
  • 動作がまだ不安定で、定期的に再起動が必要になる

私は結局のところ上記の要素が気になり、最終的にAndroid Studioで開発をしていました。
JetBrains Fleetの今後の発展に期待です。

使用したライブラリ

Ktor

APIリクエスト周辺はKtorを利用して実現しました。
KtorはすでにKMPに対応しているので、バージョンは2.3.12を利用しました。 記事執筆時点では3.0.0-beta-2も出ているらしく、活発に開発が行われている様子が伺えます。

ktorio/ktor: Framework for quickly creating connected applications in Kotlin with minimal effort

KVault

(コバルト?って読むんですかね? 知っている方教えてください!)
iOSではKeychainを、Androidではencrypted SharedPreferencesにkey-valueを保存できるラッパーライブラリです。めちゃくちゃ便利です。

Liftric/KVault: Secure key-value storage for Kotlin Multiplatform projects.

ただし現状、JetBrains Fleet上のPreviewが動作しなくなる副作用が含まれています。この副作用について当時の私はトレードオフと捉え、速度重視で導入しました。
FleetのPreviewをどうしても利用したい方にはおすすめできないかもしれません。

Failure to render Compose Multiplatform previews in Fleet with KVault dependency · Issue #55 · Liftric/KVault

EasyQRScan

Compose Multiplatform (Android/iOS)でQRコード読み取りを簡単に実装できるライブラリです。
QRコード読み取り周辺の処理について、いい感じにOS間の差異を吸収してくれます。

tekitoneではQRコードを読み取る処理が必要だったのですが、めちゃくちゃ簡単にQRコード読み取りの実装が実現できて驚きました。良いライブラリです。

kalinjul/EasyQRScan: Compose Multiplatform QR-Code Scanner

COIL

画像を非同期で読み込んで表示できるライブラリです。3.x系からKMPに対応しています。
私のプロジェクトでは3.0.0-alpha07を利用しました。

インターネット上にある画像をURLから表示する状況で便利です。

coil-kt/coil: Image loading for Android and Compose Multiplatform.

ライブラリの見つけ方

KMPに対応しているライブラリを探すときはKMP-Awesomeを利用しました。
KMP-Awesomeでは様々なライブラリが紹介されているので、必要そうなものを見つけて導入するとGoodです。

terrakok/kmp-awesome: An awesome list that curates the best Kotlin Multiplatform libraries, tools and more.

作ってみた感想

優秀!感動!

KMP+Compose Multiplatform優秀!最高!
ビジネスロジックの共有を魔法のように適用できるだけでなく、全く同じUIが異なるOSで動く。感動感激、最高です。

今回KMP+Compose Multiplatformでの開発は初めてだったのですが、慣れると意外と書けるものです。expect / actualについても書く前は難しいものと思い込んでいましたが、一度書いてみると意外と簡単で便利なものと感じました。
iOSアプリ開発をあまりしたことがない私でも今回Android/iOS向けに速度感を持ってアプリを作れたという結果は、KMP+Compose Multiplatformの優秀さの表れだと思います。最高です。

まだ実運用は難しいかも

KMPは今回のようなハッカソン的な開発では輝きますが、まだ不安定であったり技術がまだ成熟していないため実運用は難しいかもと感じました。
具体的にはまだ周辺ライブラリのKMP対応が進んでいなかったり、iOS App向けのビルドがたまに通らなくなったり、不適切な設計を行うとiOS環境でメモリリークするなどの状況があります。
(iOS App向けのビルドが動かない問題は、IDEの再起動で解決する場合が多かったです。ハッカソン期間中では根本原因の特定に至りませんでした。)

また、基本的に日本語の情報は落ちていないため、英語で調べて試行錯誤するやる気と根気が必要です。

まだK2モードは使えない

残念、今後に期待です。


今回はKMP + Compose MultiplatformでFull Kotlinなモバイルアプリを作ってみました。
KMPとCompose Multiplatformを組み合わせると全てをKotlinで記述することが可能になります。これはかなり野心的な技術構成で、書いていてワクワクします。

KMPはリリースされてから時間が経っていなく、まだまだ発展の余地を残しています。
Androidアプリエンジニアとして、KMP周辺技術の更新を今後も注意深く追っていきたいと思います!