これは Cybozu Advent Calendar 2022 の 22 日目の記事です 🎄
こんにちは、サイボウズ Office の Android エンジニアをやっている向井田 (@mr_mkeeda) です。
2021 年の夏に Stable になって以降、世界中の Android エンジニアの話題の中心になってきた Jetpack Compose は、皆さんも存分に活用されているかと思います。 サイボウズも 2022 年は Compose を本格的に運用した最初の年になりました。 そこで本記事では、サイボウズと Compose の出会いから現在の活用状況まで、そしてこれからの展望を一挙に公開します。
文量が多いので一部だけでも読めるように目次を用意しました。
出会い
サイボウズでは Compose が Google I/O 2019 で発表されたときからウォッチしておりました。
当時は iOS で SwiftUI が発表され、Flutter や React Native の人気も相まって、モバイルアプリ開発では宣言的 UI へのパラダイムシフトがやってきていました。 社内では kintone Web 開発チームがフロントエンドを React 化するプロジェクトを計画し始めた時期でもあり、Web やネイティブ問わず GUI アプリケーションは宣言的 UI で開発する時代が到来したのだなと感じていたのを覚えております。
当時の私はサイボウズの Android アプリもこの時代の波に乗ってよりプロダクト開発を加速させるべく、Compose の導入を虎視眈々と狙っておりました。 そのための準備として、Google I/O や Android Dev Summit で発表される Compose についての情報を追いかけ、社内の勉強会で共有していました。 せっかくなので当時発表した資料を公開しておきます。
この頃は Modifier
がまだ無く Composable のネストが深すぎる問題があったり、remember
が memo
という名前だったりして、今の Compose の使い心地とは違う部分がいくつかありますね。
進化を感じます。
プロダクトへの導入
Compose が Stable になってから数カ月後の 2022 年 1 月に、サイボウズ Office 新着通知の Android アプリ(以下 サイボウズ Office アプリ)で Compose を本格導入しました。
サイボウズ Office アプリでは既存の AndroidView でできた画面の置き換えではなく、新規に実装する画面で Compose を利用しています。 導入の仕方は公式ドキュメント通りなので触れずに、導入して得られたメリットについて書いていきます。
開発速度の向上
新しい技術だからという理由で導入するのは危険です。 我々は技術を取り入れる際に開発チームやプロダクトへのメリットを必ず考えています。 メリットが自明であるとしても、自分たちの扱うコンテキストでメリットを再解釈してから導入の決定をしています。
サイボウズ Office チームで Compose を導入した理由は「サイボウズ Office のモバイル利用を強化していく目的で、より素早く機能を作り価値検証をしていくため」です。 検討していた当時は、サイボウズ Office がモバイル領域でもビジネス的に勝負できるようになるまで 3 年も待てないという話をチーム内ではよく議論していました。
Compose を触ったことのある方ならご存知の通り、AndroidView と比べて Compose では同じ UI を作るために必要なコードの量がとても少なくなります。 AndroidView では、
- ConstraintLayout でうまく制約をつける
- RecyclerView で扱いやすいデータ構造
- ストリームで表現された UI 状態を購読して UI にバインドするボイラープレート
などのコードをいろいろ書かないといけませんが、Compose ではとても簡略化されています。
できるだけ少ないコードで表現したい UI を記述できる Compose は、作った機能が本当に便利なのか早く検証したいサイボウズ Office チームにはぴったりでした。
実際に サイボウズ Office アプリのリリースの回数は 2021 年と比べてすごく増えました。 2021 年はコードリプレイス作業をしていたので細かくリリースできなかった部分を差し引いても、作った機能の量は増えた印象です。
リリース回数 | 作った主な機能 | |
---|---|---|
2021 年 | 1 回 |
|
2022 年 | 5 回 |
|
完了したバックログの数も 2022 年は 2021 年と比べて増加しており、より多くの機能を短時間で作れるようになりました。 特に見積もりストーリーポイント(SP) が比較的小さい SP2 のバックログが大きく完了数を伸ばしています。 Compose によって簡単に作れるバックログが増えた効果が多少なり現れていそうです。
開発速度の向上に寄与したパラメータはメンバーの成長、新メンバーの参加、開発プロセスの改善など、 Compose の導入以外にもいろいろあるため一概には言えませんが、Compose も開発速度の向上に一役買っている感覚はあります。
UI テストの書きやすさ
Compose を導入して新たに手に入れたものがあります。 それは、UI のユニットテストです。 今までサイボウズの各 Android アプリでは ViewModel や Repository などビジネスロジックのユニットテストはできていましたが、UI の実装に対するテストコードは書けていませんでした。 Compose は AndroidView における以下のような UI テストの課題を解決してくれました。
Fragment を起動する必要がなくなった
公式ドキュメントに載ってますが、現在 Hilt は FragmentScenario
を使ったテストに対応しておらず、テストで Fragment を立ち上げる際に launchFragmentInContainer
は使えません。
It is not possible to use launchFragmentInContainer from the androidx.fragment:fragment-testing library with Hilt, because it relies on an activity that is not annotated with @AndroidEntryPoint. https://developer.android.com/training/dependency-injection/hilt-testing#launchfragment
というか、そもそも UI の実装をテストしたいのであって UI のコンテナである Fragment をテストしたいわけではありませんでした。 しかし、AndroidView の UI テストは Activity や Fragment を起動する必要があり、依存オブジェクトのモックなども大変でした。
Compose では、テスト対象はテストしたい UI ツリーを表現している Composable 関数となり、Fragment を起動する必要はありません。 また、ViewModel などの依存オブジェクトは用意しなくて済みます。 Composable 関数の実装を Stateless にしておけば、任意の状態の UI を用意でき、よりテストが容易になります。
// Fragment のテストコード例 class TopFragment : Fragment(R.layout.fragment_top) { private val viewModel : TopViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = FragmentTopBinding.bind(view) // View の設定 // ... } } @RunWith(AndroidJUnit4::class) class TopFragmentTest { @Test fun testShowNoticeCardTitle() { // テストの前提を満たすために、任意の UI 表示にするには、ViewModel のモックが必要 // さて、モックはどうやってやる? val scenario = launchFragmentInContainer<TopFragment>() onView(withId(R.id.notice_card_title)).check(matches(withText("お知らせ"))) } }
// Composable のテストコード例 data class TopScreenState( val notice: Notice ) @Composable fun TopScreen(state: TopScreenState) { // ... } @RunWith(AndroidJUnit4::class) class TopScreenTest { @get:Rule val rule = createComposeRule() @Test fun testShowNoticeCardTitle() { // 任意の状態を渡せばテストの前提を満たす UI 表示にできる val testState = TopScreenState(notice = Notice(/* ... */)) rule.setContent { TopScreen(state = testState) } rule.onNodeWithText("お知らせ").assertIsDisplayed() } }
CI での UI テストを Robolectric で安定化
Android プラットフォームに依存したテストは開発マシンの JVM を利用した Local test ではなく、Android エミュレータや実機上で動作させる Instrumented test で実施するのが基本です。 Instrumented test を Github Actions などの CI で実行しようとすると、エミュレータを十分に動作させるには Runner のスペックが足りずにテストが不安定になりがちです。 そのため、AndroidView の時代から Robolectric を使って Instrumented test を Local test として実行するテクニックが利用されていました。
Robolectric は Android プラットフォームの機能を Local test で動作させるために、Android の実装をダウンロードして利用します。 すべての Android の機能が開発マシンの JVM で正しく動くわけではないので、正しく動かないクラスは Shadow という仕組みで置き換えて再現しています。 Shadow に置き換わった部分は実環境と異なる動作になるため、Robolectric と Espresso を使った AndroidView の UI テストは動作はするものの、結構ハマりどころも多かったようです。
Compose の UI テストも公式ドキュメントは Instrumented test での実装方法となっており、CI での安定性に問題がありました。 よって、サイボウズ Office アプリでは Compose の UI テストを Robolectric を使って Local test として実行しています。
Robolectric を利用するときも Compose は AndroidView より有利な点があります。
compose.runtime
が管理する UI ツリーは、Android プラットフォームの API に全く依存しないため、Compose テストのアサーションのほとんどはプラットフォーム非依存で動作します。
つまり、Compose は仕組み上 Robolectric の Shadow の再現度による影響を受けにくいというメリットがあります。
実際、サイボウズ Office アプリでは Robolectric で動かしている Compose のテストはとても安定して動作しています。
Compose によって浮き彫りになった課題
これまで Compose の良いところを紹介してきました。 次は、既存のプロダクトに Compose を導入したことによって生じた新たな課題を紹介していきます。
Compose の外との連携
AndroidView と Compose を併用しているアプリでは、Compose 製の UI と AndroidView 製の UI を連携させたいときがあります。
例えば、サイボウズ Office アプリでは AndroidView 製の Top app bar を Compose 製の UI 内の情報で更新したい場合がありました。
Top app bar は AndroidView の Toolbar
を MainActivity の View に配置した構成になっており、複数の画面間で同じ Toolbar を使用していました。( Before の図)
この構成だと依存の関係上 ComposeView や Fragment は MainActivity の存在を知らないため、ComposeView から Toolbar の状態を変更するためにいろんなオブジェクトやモジュールを経由する必要があります。 非常に面倒です。
ComposeView 内に Top app bar があれば、State hoisting を適用し数行のコードで実現できます。
そこで、Compose で構成されている画面では MainActivity の Toolbar を使うのではなく、Compose の TopAppBar
を使って Compose だけで完結できるようにしました。( After の図)
After の方法だと Toolbar と TopAppBar を切り替えて表示するのですが、切り替わるタイミングで画面上がちらつくため見栄えは良くないです。 早くすべての Top app bar 実装を Fragment 側に移したいです。
Top app bar を誰が持つのか問題は Android アプリ開発では定番の問題ですが、我々の場合は Compose の導入によってこの問題と向き合う必要性が出てきました。 私個人の意見ですが、Compose では Top app bar を含めた UI のほとんどは一瞬で作れるため、各画面が UI のインスタンスを保持したほうが後々楽になります。 共通モジュールなどで UI の実装を共有する程度で十分だと思いました。
モジュール間の画面遷移
マルチモジュール構成のアプリにおいて Navigaiton ライブラリを使う際、xml の NavGraph ではモジュールを跨いだ画面遷移に Deep link を使うよう案内されています。
個人的にこの Deep link の方法は、以下の理由でイマイチだなと思っています。
- xml と Kotlin コードで Deep link URI を共有するのが大変そう
- モジュール間の暗黙的なコミュニケーションになっていて、バグを生みそう
- Safe Args が使用できないため型安全ではない
すべての画面が Compose 化できると、以下の図の構成にできて上記の問題すべてが解決できます。
- 画面遷移イベントを Lambda で上位 Composable にリフトアップすれば、Deep link を用いずに済む
- モジュール間の繋がりはすべてのモジュールの存在を知っている :app モジュールで定義するため、モジュール間コミュニケーションが追いやすい
- Lambda や関数定義による型安全が手に入る
この方法は Kotlin DSL での NavGraph 定義に加えて、Fragment の画面遷移を撲滅する必要があるため、規模の大きいアプリほど実現が大変になります。 しかし、その分メリットは大きいです。 サイボウズ Office アプリはまだ AndroidView 製の画面を Compose へ移行できていないためこの方法は選べませんが、いつか実現させたいと思っています。
詳しい実現方法はこちらのドキュメントに書いてあります。
- Type safety in Kotlin DSL and Navigation Compose | Android Developers
- Common modularization patterns | Android Developers
おわりに
本記事ではサイボウズ Office での取り組みを中心に紹介しましたが、kintone や Garoon など他の製品でも Compose を活用する動きがあります。 そして、本記事に書ききれなかった Compose のネタもたくさんあります。
そんなこんなで、来年も Compose のネタをどんどん社外に発信していきたいと考えています 📝 もし興味がある方は、以下の媒体でサイボウズの Android アプリ開発について発信しているので、ウォッチしていただけると嬉しいです!!
- この Cybozu Inside Out ブログ
- Cybozu Inside Out YouTube チャンネル
- Twitter ハッシュタグ #cybozu_android