タクシーアプリ「GO」のAndroidアプリを開発している山本です。本記事ではAndroid アプリの UI 開発ツールキットである Jetpack Compose ライブラリをプロダクトの機能開発を進めながら導入している事例をご紹介します。
Jetpack Compose は新しく Google が提供している Android アプリの UI 開発ツールキットです。以下のような特徴を持ち、Google I/O 2022 イベントでも多くのアップデートが発表されました。
「GO」の Android アプリはプロダクト成長のために継続的な機能開発をおこなっています。機能開発のスピードを落とさないように気をつけて、チームメンバー全員で Jetpack Compose を導入しています。
Jetpack Compose を導入することで、機能開発とともに長くなるビルド時間や、サービスの性質として避けられない複雑な状態管理について解決できればと筆者は考えていました。 一方で既存アーキテクチャ構成に対してどのように Jetpack Compose を導入するか、機能開発のスピードにどこまで影響があるかという不安点を確認するために、まずはデバッグメニューへの導入をおこないました。
初めに Jetpack Compose の導入方法を検討するための動作検証としてデバッグメニュー画面の UI を実装しました。
デバッグメニュー画面は開発版アプリのみ表示するため、実際にリリースされるアプリに影響を与えずに動作検証が可能です。また機能開発をサポートするデバッグ機能はシンプルな UI で実現するため、慣れない Jetpack Compose でもスムーズに検証を進めることができました。
UI レイヤを Android View から Jetpack Compose に移行する場合は、アーキテクチャ構成や機能開発スピードに対してはあまり影響がないことを確認できたので、次は実際にリリースされる機能への導入を進めることにしました。
機能開発にあわせて既存画面に追加する一部の UI のみ Jetpack Compose で実装しました。
Jetpack Compose の相互運用API を使用し、機能開発に関連する一部 UI のみ CustomView の要領で Jetpack Compose で実装します。もし追加 UI の表示バリエーションが複雑だった場合もプレビュー機能を活用することで、素早く実装中の UI を確認できます。 また既存画面のレイアウトとしては ComposeView を追加するだけの少ない差分で済むため、影響範囲を抑えつつ Jetpack Compose を導入することができます。
<layout ... >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 既存レイアウト -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/new_feature"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/previous_feature"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
binding.newFeature.apply {
// ComposeView を表示する親の View にあわせてライフサイクルを設定する
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
// `@Composable fun NewFeature` として定義した UI を ComposeView に表示する
setContent {
MdcTheme {
NewFeature(newFeatureState)
}
}
}
機能開発にあわせて画面を一から作るタイミングで画面全体を Jetpack Compose で実装しました。
今まで Fragment で実装されていた画面の UI 実装を XML の代わりにすべて Jetpack Compose で実装します。画面単位の UI を実装するため、そのまま画面単位でプレビュー機能を活用することができます。 また Jetpack Compose による UI 実装を進める上で画面遷移や状態管理、DI などを考慮する必要がありますが、今回は Fragment の UI 実装のため普段から使っているアーキテクチャ構成をそのまま使うことができます。
Jetpack Compose 向けの Navigation や ViewModel , DIライブラリ等の機能拡張ライブラリも提供されています。現時点ではまだ導入せず既存アーキテクチャ構成の延長として実装していますが、段階的に導入できないか検討を進めています。
UI 実装への Jetpack Compose 導入を進めると、共通ボタンや独自ローディング表示など「よく使われる共通 UI 」を Jetpack Compose に表示したい場面が出てきます。
共通 UI が既存の Android View として実装されている場合も相互運用API にある AndroidView を使うことで実現できますが、Jetpack Compose の特徴であるプレビュー機能が使えなくなり UI 確認の効率が下がってしまいます。そのため共通 UI の Jetpack Compose 化を進めました。
共通 UI は Android View と Jetpack Compose それぞれ両方の画面から使われるため、同じく両方に向けて View クラスと Composable メソッドを定義する必要があります。 1つの共通 UI を2種類の方法でそれぞれ実装すると、挙動の差異が発生する可能性がありメンテナンスの手間も増えてしまいます。
そこで Compose の既存の UI との統合 を参考に下記アプローチで 共通 UI の Jetpack Compose 化を進めることにしました。 Android View 向け実装の内部から Jetpack Compose 向け実装を参照することで、共通 UI の実装が1つにまとまり、プレビュー表示もできるようになります。
/** Jetpack Compose 向け共通ボタン */
@Composable
fun MainHapticFeedbackButton(
onClick: () -> Unit,
enabled: Boolean,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
// 触覚フィードバックとアニメーションをおこなうButton
Button(...)
}
/** Android View 向け共通ボタン(内部は Jetpack Compose 版を参照している) */
class HapticFeedbackButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
@Composable
override fun Content() {
MdcTheme {
MainHapticFeedbackButton(...)
}
}
}
継続的な機能開発の中でも Jetpack Compose の恩恵を受けることができました。 まだまだ改善できる点は残っているので、よりよい開発環境を整えていきたいと思います。
参考:タクシーアプリ「GO」AndroidにできるところからJetpack Composeを入れている話 https://speakerdeck.com/gya/introduce-jetpack-compose-gradually
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!