Androidエンジニアの北川と藤原です。9月11日〜13日に開催された DroidKaigi 2024 に参加してきました! 3日間濃密な時間を過ごしてきましたので、その内容をレポートします。
ミラティブは 2022 年から DroidKaigi に協賛しており、今年で3年目を迎えます。
Workshop Day
1日目は、JetBrainsのSebastian Aignerさんによるワークショップ「From 0 to 100 with Kotlin and Compose Multiplatform」が開催されました。
こちらは、Kotlin を活用したマルチプラットフォーム開発について、実際に手を動かしながら学ぶワークショップです。 前半は Kotlin Multiplatform、後半は Compose Multiplatform の2部構成となっていて、基礎から実践までを一通り確認できる内容となっていました。
前半: Kotlin Multiplatform(KMP)
Kotlin Multiplatform(KMP)は、複数のプラットフォーム間でコードを共通化することで、一貫性を保ちながら、重複コードの削減と効率的な機能開発を可能にします。
KMP を採用する理由として、次のポイントが紹介されました:
- (自分たちは)既にKotlinを知っている
- 既存のKotlinコードを活用できる
- 各プラットフォームでネイティブ動作が可能
- 共通化する機能としない機能を選択できる
- 既存のプロジェクトへの追加が容易
- オーバーヘッドなしでプラットフォーム固有機能へアクセス可能
KMPの強みは、追加の学習コストが低いこと、柔軟に適用範囲を選べること、そしてネイティブレベルの高速なパフォーマンスを実現できることにあるそうです。
1つ目のタスクでは、各プラットフォームに合わせたデータの読み書きロジックを実装しました。具体的には、JVMではファイル、AndroidではファイルまたはSharedPreferences、WasmではlocalStorageをそれぞれ使い、actual修飾子をつけて実装します。以下はその実装例です。
// commonMain expect fun persistString(key: String, value: String) expect fun restoreString(key: String): String // jvmMain actual fun persistString(key: String, value: String) { File(“$key.txt”).writeText(value) } actual fun restoreString(key: String): String { return File(“$key.txt”).readText() } // androidMain val context: Context get() = ContextHelper.currentContext!! actual fun persistString(key: String, value: String) { context.getSharedPreferences(“mypref”, Context.MODE_PRIVATE).edit().putString(key, value).apply() } actual fun restoreString(key: String): String { return context.getSharedPreferences(“mypref”, Context.MODE_PRIVATE).getString(key, ””) ?: “” } // wasmJsMain actual fun persistString(key: String, value: String) { localStorage[key] = value } actual fun restoreString(key: String): String { return localStorage[key] ?: “” }
Kotlinに慣れた開発者にとって、目新しい部分は expect と actual 修飾子、そしてプラットフォーム固有のAPIくらいで、学習コストが非常に低いことがわかると思います。
また、Ktor を使用したAPIリクエストの共通化も題材として取り上げられ、少ないコードで複数のプラットフォームをカバーできるKMPの強みを再確認できました。
後半: Compose Multiplatform(CMP)
Compose Multiplatform(CMP)は、Android の宣言的UIフレームワークである Jetpack Compose を、複数のプラットフォームで使えるようにする取り組みです。 CMPを使うと、ロジックに加えUIの実装まで、最大100% Kotlin でマルチプラットフォームのアプリケーション開発を行うことができます。
記事執筆時点で、AndroidとDesktopはStable、iOSはBeta、WebはAlphaとなっており、アクティブに開発が進められています。
具体的に各プラットフォームでどのように実現されているのかという点についても説明があり、
- Android: ネイティブのJetpack Compose APIにマッピング。
- Desktop: Skiko/Skia(レンダリングエンジン)で描画され、JVMベース。
- iOS: Kotlin/Nativeのバイナリで動作し、Skiko/Skiaで描画。120Hzもサポート。
- Web: Kotlin/Wasmで動作しWasm-GCを使用。DOMベースではなくCanvasベースで描画。
とのことでした。
実際に、JetBrains社製のIDEを簡単にインストールできるツール「JetBrains Toolbox」は Compose For Desktop を用いて開発されているそうです。100万人以上が利用しているアプリケーションで使われているという事実には、十分な説得力があります。
タスクでは、commonソースセット内でUIを実装し、Desktop、Android、Wasm の3つのプラットフォームでの動作を確認しました。また、Compose Resourcesを使用し、背景画像や多言語対応文字列の設定も行い、CMPエコシステムに対する理解を深められました。
終盤では Lifecycle、ViewModel、NavigationのMultiplatformサポートに関する最新情報も紹介され、ワークショップのタイトル通り、0から100までKMPとCMPを満喫できました!
Conference Day
2日目と3日目のConference Dayでは、合計で49ものセッションが行われました。 その中から特に気になったものをピックアップしてご紹介します!
北川編
From Idea to IDE: Developing Plugins for Android Studio
INFINUM社の Ahmed Ali さんによるセッション。
IntelliJ IDEA Pluginの開発方法を扱うこのセッションでは、クラスのコードをGeminiに読み込ませ、ドキュメントコメントを自動生成するコンテキストメニューを追加する例が紹介されました。
Actions、PSI、Services、Threading Model の4つの基礎概念を段階的に解説しており、IDEA Plugin開発の第一歩となる有益なセッションでした。プラグインUIの作成方法としては、公式のKotlin UI DSLを使用する方法と、Jetpack Composeを用いる方法が紹介されました。ただし、Jetpack Composeは現時点では公式にサポートされておらず、IntelliJに近い見た目を実現するためには、Jewelという専用のテーマを適用する必要があるとのことです。
私自身、以前Jetpack Composeを使ったIDEA Plugin開発に挑戦した際は、画面の更新がうまくいかないなどの問題に直面し、挫折した経験があります。しかし、今回のセッションを通じてプラグイン開発に対する理解が深まったので、再度挑戦してみようと思いました!
KSPの導入・移行を前向きに検討しよう!
ポート株式会社のしゅんいちさん(@shxun6934)によるセッション。
Googleが開発する軽量なコンパイラプラグインであるKSPを主題に、アノテーションプロセッサーの歴史から、kaptとKSPの比較、そしてKSPの実装法とその将来性まで、要点を一通りカバーした構成となっていました。特に、kaptが生成する @kotlin.Metadata の各種パラメータの意味や、KSPにおけるファイル表現である KSFile の内部構造など、言語のエコシステムに深く関わる内容が多く、とても楽しめました。
Ask the speaker では、KSP処理中にシンボルに対応するソースコードにアクセスする方法について質問しました。結論としては、KSNodeが持っているLocationにファイルパスと行番号が定義してあるので、こちらを利用してソースコードを自力で切り抜く必要があるとのことでした。後日調査したところ、K2から導入される Kotlin Analysis API では、PSI 要素へのアクセスが可能なようでしたので、KSPを経由せず直接Analysis APIを使えば、より柔軟に実装できるかもしれません。
個人的に嬉しかったのは、登壇者の方が私の Kotlin Fest 2024 でのセッションを見てくださっていたことです。 言語の根幹に関わるコンパイラー周辺領域を深堀りしている方と繋がることができて、とても良かったです。
Kotlin Fest 2024 のアーカイブ動画も公開されていますので、ご興味がありましたらぜひそちらもご覧ください!
藤原編
テストの新時代(A New Era of Testing)
android-junit5の開発者でもあるmannodermaus氏のセッション。テストの観点でAndroidコミュニティに大きく貢献をした方だと聞いていたので、この機会を逃すまいとセッションに参加してきました。
Android-JUnit5について
JUnit5はJava8を利用しているためAndroid26以上の端末でしか動作しません。 Android25以下の端末を利用してテスト実行した際にもクラッシュはせずテストがスキップされるようです。
Parameterized Argument Sources
テスト用のパラメータをアノテーションから入力でき、同じテストの複数入力値での記述を簡潔にできます。
@ParameterizedTest @ValueSource(strings = { "apple", "banana", "cherry" }) void testWithStringParameter(String fruit) { assertNotNull(fruit); }
上記サンプルは@ValueSourceで値を渡していますが、EnumやCSVなど多様な入力方法をサポートしています。
Conditional Test Execution
テストの実行環境にて、特定の条件下でのみ実行させるよう制御する機能。
// CI環境でのみ実行させる @Test @EnabledIfEnvironmentVariable(named = "CI", matches = "true")
のようにすることで実行環境を制限できます。SDKバージョンであったり、端末メーカー、flavorでの絞り込みも可能なようです。
Parallel Test Execution
テストを並列実行可能にする機能です。 特定のテストの処理を並列処理にして実行時間を短縮できますが、その後調べたところスレッドセーフである必要があったりNon-UIのテストしかサポートしてない、など制限が多そうな感じがしました。使い方を考えないと思ったより短縮効果は望めなさそうです。
Robolectricについて
これまではJUnit5でRobolectricを動かせなかったが、現在はExperimentalだがapter-tech/junit5-robolectric-extensionを導入することで一部ケースは動かせるようになったとのこと。 JUnit5でRobolectricをサポートすることについて、以前は「こんなクレイジーなことをやる人はいない」と言っていたが、拡張機能を作った「Balázsはクレイジーなやつだ」というのは会場も笑っていました。 junit5-robolectric-extensionではCompose unit testはクラッシュしEspressoを使うとデッドロックするようで、まだその辺のサポートはできていないとのことでした。
感想
現在はJUnit4でテストを書いていて、個人的にもJUnit5はドキュメントレベルでしか見ていなかったので大変勉強になりました。 制限はあれど、テストコードの可読性向上や簡潔に書けるようになったり、実行時間短縮など便利な機能が多いためテストへのモチベーションが向上するセッションでした。
Jetpack ComposeにおけるShared Element Transitionsの実例と導入方法 またその仕組み
DeNA社のhyoga氏のセッション。新しいSharedElementを既にプロダクトでも導入し始めているということなので、他社事例に興味がありセッションに参加してきました。
Compose 1.7.0-beta01で(やっと)公式がAPIを用意してくれたShared Elementですが、本セッションでは基本的な使い方とプロダクション相当のアプリに導入した際にどういった課題が起きて、どうやって解決したか説明されています。
対象となるUI
Grid上の一覧画面に画像を並べて、タップすると詳細画面で画像を拡大表示し、さらに横スワイプで別の画像に切り替わる、というもの。 さらに詳細画面から戻る際はバックキーの他にドラッグすることでも一覧画面に戻ることが可能。
Modifier.sharedElementとModifier.sharedBounds
気になったのはModifier.sharedElementとModifier.sharedBounds、2種類のModifierが用意されていること。
- sharedElement
- 前後の画面で同じ要素を共有する。アニメーションは遷移後のものを利用。
- sharedBounds
- 前後の画面で異なる要素を共有する。アニメーションは前後の要素がそれぞれIn/Outしていくらしい。
シームレスに形がかわるもの(要素がTopAppBarに変わるやつとか?)は作れないそうでsharedBoundsで誤魔化しながら作るしかないとのことでした。
単純なものはsharedElementで複雑なものはsharedBoundsという考え方でよさそう。
- 前後の画面で異なる要素を共有する。アニメーションは前後の要素がそれぞれIn/Outしていくらしい。
シームレスに形がかわるもの(要素がTopAppBarに変わるやつとか?)は作れないそうでsharedBoundsで誤魔化しながら作るしかないとのことでした。
発生した課題と対策
- SharedElementと他アニメーションが競合する
- SharedTransitionScope.isTransitionActiveでアニメーションを制御
- 一覧画面に存在しない画像から戻る場合にアニメーションが発生しない
- 前後の画面に同じ要素が表示されている必要があるため、双方のデータを同期させる
- 同じComposable内でSharedElementを実装した際に表示崩れする
- SharedElementのKey管理がバグっている可能性があるので見直す
感想
最近は機会が減少していて実装することがなかったSharedElementですが、リッチなUIを作ろうと思ったとき優先的に挙がってくる印象があります。Betaの段階ではプロダクトで使うことはないですが、Stableになってもし導入することになったときに備え、特にsharedBoundsの方は試しておきたいと思いました。
公式アプリへのコントリビュート
OSSとして開発されているDroidKaigi公式アプリに、今年も北川がコントリビュートしました! 具体的には、検索画面のベース実装の追加と、タイムテーブルに現在時刻を表示する機能などを担当しました。 Composable関数として定義されるPresenterや、Roborazziを使ったRobotパターンのテストまで、 プロジェクト全体を見渡すことができ、とても有意義な経験となりました。
RepositoryのデータをComposable関数としてUIに渡す実装は初めて見ましたが、 実際に触ってみたところ好感触でしたので、個人で開発中のプロジェクトにも似たアプローチを導入してみたりしています。
また、DroidKaigi2024のアプリには見かけ上ViewModelが存在しません。
これは、Rinというライブラリの中に定義されている rememberRetained
などの関数内に隠蔽されているためで、
実際にソースコードを見てみると、RinViewModel
が存在することを確認できます。
こちらもとても興味深いライブラリなので、今後社内勉強会などで深堀りしてみたいと考えています。
おわりに
今年のDroidKaigiは例年に増してKotlin・Androidエコシステム全体の進化を感じる内容となっていました。 Kotlin 2.0やKotlin Multiplatform、Jetpack Composeの新しいノウハウ、さらにはGeminiをはじめとする生成AIなど、最新の技術に追従するセッションが多く、時代の変化を肌で感じます。
セッションのアーカイブ動画は既に全て公開されているとのことなので、当日見られなかったものも含めてじっくりと見ていきたいですね。 また、Kotlin・Androidの更なる成長に期待して、今後も新しい技術を積極的にキャッチアップしつつ、コミュニティの一員として技術力を磨いていけたらと思います!
We are hiring!
ミラティブでは一緒に開発してくれるAndroidエンジニアを募集しています! 少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、お気軽にご連絡ください。
インターンも募集中です!