Jetpack Compose を少しずつアプリに導入する方法と得られた知見について

この記事は、ドワンゴ Advent Calendar 2021 の15日目の記事です。

N予備校 Android チームでは、アプリの各画面を少しずつ Jetpack Compose で書き換えています。

developer.android.com

この記事では、チームで Compose を学習した過程や、段階的な Compose 導入の方法について、また導入して良かったことと、実装でつまづいたことをまとめます。

これから Compose の導入を考えている方や、具体的な導入事例を知りたい方の参考になれば幸いです。

Composeの学習について

アプリに Compose を導入するために、まずはチームでの学習から始めました。学習内容は、Jetpack Compose learning pathway を参考に、各 Google Codelab の学習を進めました。

以下に、学習した Codelab と、チームで出た感想をそれぞれまとめます。

  • Jetpack Compose basics
    • Compose の書き方は Flutter に似ている。
    • RecyclerView が簡単に書けるようになってとても助かる。
    • Modifier の各要素の定義順によって結果が変わるのは難しそう。
  • Layouts in Jetpack Compose
    • CircleShapeで円が描画できたり、RoundedCornerShapeで矩形の角が丸まるのは便利。
    • coil という画像ライブラリが Compose では便利そう。既存の Glide から置き換えてもいいかも。
    • カスタムレイアウトがとても難しい。心が折れそうだった。
  • Using state in Jetpack Compose
    • TextFieldはEditTextのように内部状態を持っていないので不整合が起きにくい。
    • テストや使い回しを考えると、なるべく stateless composable にしておくのが良さそう。
    • Livedata を State として利用できるのは Compose に移行するときに助かるかも。 .observeAsState() で初期値を指定できるのも嬉しい。
  • Jetpack Compose theming
    • テーマ定義の仕方が XML でやる時と比べて簡単そう。
    • Theme を活用するには、明確なルールに沿ったデザインが必要になると思う。
  • Jetpack Compose Animation
    • アニメーションのためにさまざまな処理が準備されているのはありがたい。
    • Android Studio の Interactive Preview ですぐにアニメーションを確認できて便利。
  • Jetpack Compose Navigation
    • Fragment を使わずに画面遷移を行うのが衝撃だった。
    • Fragment Lifecycle から解放されることで助かる部分も多いと思う。
    • 画面遷移のテストが簡単にできるのは心強い。
  • Testing in Jetpack Compose
    • チートシートが便利。
    • UI テストが増えすぎて、CI の実行時間が長くなるのが心配。
  • Accessibility in Jetpack Compose
    • 今までアクセシビリティを考慮して実装したことがなかったので勉強になる。
    • Accessibility Scanner で各画面のアクセシビリティを確認してみたい。
  • Migrating to Jetpack Compose
    • xml の各要素を徐々に <ComposeView> に置き換えていけば、スムーズに Compose に移行できる。
    • 既存の画面を Compose に置き換えながら、それぞれテストを書いていきたい。
  • Advanced state and side effects in Jetpack Compose
    • 副作用とは、Composable の外で発生するアプリの状態変化。Composable は副作用が無いべきだが、スクリーンを開く時は仕方ない。

上記の Codelab の学習を、普段の業務と並行しながら約1ヶ月半かけて行いました。

学習形式は、当初は各自で業務時間から自由に1時間を使って学習を進めて、学んだことや感想をチームに共有するようにしていました。ただそれだと、忙しい日には時間の確保が難しくなったので、途中からは画面共有をしながら1日30分ほど、全員で同時に学習を進める形にしました。

画面共有をする形にしたことで、疑問があったときにすぐにチームで質問できたり、コードを書く様子を共有できたりして、学習効率が上がったのではないかと思います。

Compose を導入する

実際に Compose を導入するにあたっては、以下の手順で進めました。

1. 書き換えの対象として、使用頻度が少なくてシンプルな画面を選ぶ

Composable への書き換えは、ユーザーの使用頻度が低く、かつ View の構成がシンプルな画面から始めました。理由は、Compose への移行によって思わぬ不具合が発生したときの被害を最小限に抑えるため、そして新しい技術への拒否反応を抑えるためです。

いきなり多くのユーザーが使う画面や、重要な画面の書き換えを始めるのはリスクが高いです。チームの習熟度が高まるまでは使用頻度が少ない画面から書き換えを行いました。

また、複雑な画面から書き換えを始めると、うまく実装できなかったり、そもそも Compose がまだ対応できていない View だったりして Compose で書くことにストレスが溜まってしまう可能性もあります。なので、まずは簡単な画面から書き換えることで、Compose で書くのは効率的で面白い、という自信をチームでつけていこうと考えました。

ちなみに、N予備校アプリでは VR 機器との連携情報を表示する「VR連携アプリ確認画面」から Compose 化を行いました。

f:id:hiraike32:20211213174759p:plain
VR連携アプリ確認画面

2. 画面の各部品を Compose で書き換えて、View を共存させる

画面を決定したら、画面の各部品を1つずつ Composable に置き換えていきます。

画面全体を一気に Compose に書き換えるのもリスクが大きいので、以下のドキュメントを参考に xml に定義されている View を1つずつ <ComposeView> に書き換えていく形を取りました。

developer.android.com

「VR 連携アプリ確認画面」で言うと、画面上部の TopBar と、連携しているアプリを一覧している RecyclerView、そして連携を解除したときの SnackBar の3つの部品が存在するので、それぞれ Compose 化をします。

<!-- xml ファイルを少しずつ Compose に書き換える -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- TopBar を表示している -->
    <include
        android:id="@+id/toolbar"
        layout="@layout/toolbar" />

    <!-- RecyclerView を先に ComposeView に置き換えた -->
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dp_32"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar" />

    <!-- SnackBar を表示する場所 -->
    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/snackbar_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

一時的に View と Compose が共存しますが、このやり方だと着実に Compose への置き換えを進められて良かったです。

f:id:hiraike32:20211213174951p:plain
各部品を1つずつ Compose に置き換えていく

3. Fragment から xml を削除する

そして、画面の全ての部品が Compose で書き換えられたら、xml を削除して Fragment で Compose を展開するようにしました。

class NyobiFragment : Fragment() { 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // 作成したComposable を表示する
            }
        }
    }
}

将来的には Navigation Compose を使用して Composable の中で画面遷移もできたらいいなと思うのですが、現時点では Fragment のライフサイクルに紐づくロジックが多いので、Fragment による画面遷移は維持していく方針です。

このように簡単な画面から部品を1つずつ置き換えていくことで、無理なく Compose に馴染めたと思います。現在は複雑な画面の置き換えにも取り組んでおり、アプリ全体で順調に Compose 化が進んでいます。

Compose を導入して良かったこと

Compose を導入して良かったこととして、以下のことがあります。

View のテストが簡単に書けるようになった

今までのN予備校では、View のテストがほとんどない状態でした。そのため、見た目の修正を行なったときは、実際にアプリを動かして確認するしかありませんでした。そもそも、既存の View に対するテストを行うことは技術的になかなか難しかったように思います。

それが、Compose だと全て Kotlin で完結するようになるので、テストが格段に書きやすくなりました。実装方法については以下のドキュメントが参考になります。

developer.android.com

N予備校では、主に以下のようなテストを作成しています。

  • 表示に対するテスト
    • ログアウトしているときは、ログインしているのときのメニューが表示されないこと
    • 教材の進捗度が適切に表示されること
  • 状態変化に対するテスト
    • 通信中はローディング用の表示がされること
    • 必要な項目の入力が完了していたら、質問投稿ボタンが押せること
  • 制限に対するテスト
    • 入力欄には16文字以上は入力できないこと

テストを書くことで、修正を入れたときにアプリを動かして確認する必要がなくなったのはとても助かります。また、将来の修正にも強くなっているので、Compose を導入して良かったなと思いました。

Kotlin で見た目も操作も書けるようになった

今までの View の実装だと、見た目は xml に書いて、タップされたときの動作やアニメーションなどは Kotlin で書く、みたいな分業になっていましたが、これが Compose だと Kotlin で完結するのでとても書きやすくなりました。

特にその恩恵が大きいのがリストの表示です。今までは xml に RecyclerView を用意して、Adapter を継承して、各メソッドを override して、みたいな煩雑な処理が必要でしたが、Compose では LazyColumn または LazyRow を書くだけで済んでしまうのがとてもありがたいです。もう xml には戻れないですね...。

Compose の実装でつまづいたこと

逆に Compose を導入してつまづいたこととして、以下のことがあります。

プレビューのために時間がかかる

Compose で作成した見た目をプレビューするためには、アプリのビルドが必要になります。Kotlin で書いているので仕方ないのですが、xml だとリアルタイムでプレビューできたのを考えると、ちょっと大変になりました。少しの修正でもアプリをビルドしなければならないのは辛いところです。

Android Studio Bumblebee ではビルド速度の改善が見込まれているようですが、それでもプレビューは xml よりも時間はかかると思います。そもそもアプリのビルドを高速化しないといけないですね。

まだ準備されていない View がある

2021年12月現在で、Compose がまだ対応していない画面や機能がいくつかあります。私たちが直面したものは以下です。

  • WebView
    • Compose が対応する気配がないので、AndroidView として使うしかなさそう?
  • Linkify(TextView から URL を抽出してリンクをつける util)
    • BasicText などで用意されているかと思いましたが、見つかりませんでした。AndroidView として使っています。
  • ViewPager
    • Accompanist に Pager layouts として用意されていますが、まだ Experimental なので不安なところです。

他にもまだ対応されていない View はあると思いますが、これらは Android View を使いながらうまく付き合っていくしかないですね。

まとめ

大変なこともありますが、総じて Compose は素晴らしいと思います。さらなる進化に期待しつつ、N予備校アプリでは Compose への書き換えを今後も積極的に進めていきます!

まだ Compose を導入されていない方は、少しずつでも価値があると思うのでぜひ検討してみてください。

We are hiring!

N予備校 Android チームでは、一緒にアプリ開発を進めていただける方を募集しています。

カジュアル面談も行っていますので、この記事の内容について、また Android チームの開発体制について興味がありましたら、ぜひお気軽にお話ししましょう。

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

N予備校の他チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com