Jetpack Composeを採用したkifutown Androidアプリについて

Riku Maehara
ARIGATOBANK Tech Blog
Oct 21, 2021

Androidチームの前原 (@maehara_08) です。kifutown AndroidアプリはJetpack Composeを全面的に採用して作られております。本記事ではJetpack Composeを使用する上でどのようなことを考えながらアプリを作ったかについて触れられたらと思います。

Jetpack Composeは2021年7月にStableになった新しいライブラリです。チームで勉強し、挑戦しながら進めている部分もあり、誤っている部分もあるかもしれませんが温かい目で読んでいただけると嬉しいです。

なぜJetpack Composeを採用したのか

Jetpack Composeは宣言的にUIを構築できます。強力で便利なKotlin DSLを使用することによりシンプルで少ないコードでUIを構築できます。

宣言的にUIを構築できるという点が大きなポイントであり、従来のAndroidでのViewの世界と大きく異なる点があります。それは、従来は「View自体が状態を持っていた」ということです。EditTextは自分が入力されている文字の状態を持ち、チェックボックスは自分が✅されているか否かという状態を持っていました。一方Jetpack ComposeにおいてEditTextは状態を外から与えられた値を表示するのみで、自分自身に文字が入力されても何も変化しません。チェックボックスも同様に、自分がクリックされても外から状態を与えないと自分に✅が付きません。

これらの特徴により、開発者はUIを構築するにあたり、外部から与える状態をコントロールするだけでよいという恩恵を与えてくれます。結果、ロジックを切り分けやすく、状態が比較的シンプルになりコードの量と開発体験の改善が見込めると考えました。

Composeの気持ちに寄り添いながら設計する

Jetpack Composeは従来のActivity/Viewのライフサイクルとは別でComposableのライフサイクルを持っています。

Composableは引数が更新された場合にRecomposeされます。不要なRecomposeを走らせないためにも、Composableの引数について、何が更新されて、されないのかに気を配りながら構築していきます。

例えばComposableの引数をViewModelにして引き回すと、思わぬViewModelの差分がトリガーとなって不要なRecomposeが走ることがあります。kifutown Androidでは描画に必要な情報を詰めたStateと、viewでのEventのコールバックを集めたinterfaceをComposableに渡す設計としました。

ボタンを押すとTextの数字をインクリメントする簡単なカウントアプリを一例に、実際の実装を見て行きます。

作りたい画面はこちらです。

画面を元にStateを考えてみます。動的に変わる部分であるカウントする数字をStateで表現してみます。

続いてViewで発生するEventを元にViewEventListenerを考えます。今回はButtonを押したときのコールバックが必要です。

このinterface実装先は安定していることをComposeに教えてあげるために@Stableを付けます。Recomposeの基準などについては公式ドキュメントにも載っていますので一読することをおすすめします。

ViewModelはStateFlowとviewEventListenerを公開します。

今回はCoroutine Flowを使っています。onClickCountButtonの実装として受け取った値をincrementしてemitするという処理にしています。

残るはComposableの部分です。

uiStateとviewEventListenerを引数に取るComposableを作り、状態を保存するviewModelと分離しています。viewEventListenerにはStable Annotationが付いているので個別に関数objectを渡さなくてもRecomposeを抑えることができます。自分は今までViewがいつ再描画されるかを特別意識しながら構築したことが少なかったと感じています。どういうタイミングでRecomposeされるか、Composableの気持ちに寄り添いながら構築することが一つのポイントかなと思います。

Tips

画面表示時に処理をしたい

kitutown AndroidはNavigation composeを使ってSingle Activity, Fragment無しで作られています。ComposableにはonResumeのようなイベントはないので、やるとしたらComposable入場のタイミングを使うことになりそうです。画面遷移のタイミングでNavigation composeによってcomposableで定義されたPathのComposableが退場/入場していますので、画面TopのComposableの入場が画面表示時と同義です。

LaunchedEffectは入場時に一回呼ばれるEffectですが、suspendを起動する役割も備えているため、入場時に一度呼ばれる機能のみが必要なので作ることにしました。

LaunchedEffectを参考に作ってみました。なお、退出時にキャンセルがしたい場合はDisposableEffectも選択できます。

画像を抽象化したい

アプリ内で使われる画像は様々な形式があります。

  • http url
  • bitmap
  • drawable res
  • ImageVector ← new!
  • etc…

Jetpack ComposeになってImageVectorも仲間入りしました。ImageVectorは予め用意されているマテリアルアイコン群のクラスとして利用されています。

ViewModelやView側で取り回しを良くするため、ImageResというInterfaceで抽象化しました。Image ComposableはPainterという抽象クラスで描画することができるため、Painterを取得できるInterfaceにすればよさそうです。

上記はImageVectorの一例ですがこのように中身が何であろうとPainterを取得してくれるようなInterfaceを用意しておくと便利です。ポイントはComposable関数としてinterfaceのfunctionを定義することです。

Composableからしか呼べない値を定義したい

getterに@Composableをつけることで実現できます。

MaterialTheme.colors から値を引きたい場合等、意外と便利です。

Jetpack Composeでもアレが使いたい

Jetpack Composeのかゆいところに手が届くライブラリ群、Accompanistに便利なツールが揃っています。例えばFlexboxのCompose版であるFlowLayoutが用意されていたり、Pull to RefreshがしたいときのためにSwipeRefreshが用意されたりしています。他にも便利ツールが揃っているので覗いてみると面白いです。

不要なRecomposeを検知する方法について

Stateが更新されたタイミングで、なにかComposableの設計などにミスがあると不要なRecomposeが走ることがあります。Recomposeを検知する方法を考えたところ、

  1. Recomposeされた回数を出力するLogを仕込む
  2. RecomposeされるとFPSが落ちるのでFPSを表示しておく

1はLogを分析すれば正確に不要なRecomposeが分かりそうです。分析するのは開発者がLogcatを見るという方法でしょうか。

2は常にFPSを表示しておけば、動作確認しながら明らかにFPSが落ちるタイミングに気づく事ができます。例えばモンキーテスト用のパッケージでFPS表示をするように設定すると、非エンジニアでもFPSが落ちるタイミングに気づくことができます。欠点は、FPSが落ちないほどの不要なRecomposeには気づきにくいことです。

今回致命的なパフォーマンスの低下に早く気づきやすいという点から、2を導入しました。

実際に

  • 多くのTextFieldがある画面で一文字入力するごとにすべてのTextFieldでRecomposeされるため遅くなる
  • LazyColumnでスクロールすると不要なRecomposeが行われて遅くなる

等、不要なRecomposeが原因でのパフォーマンス低下に気づくことができました。

最後に

Jetpack Composeを導入して品質の高い画面をより早く作ることが可能になったと実感しております。

ARIGATOBANK AndroidチームはJetpack Compose導入に限らず、開発体験がどうすれば良くなるか、どうすればよりよいサービスになるのかを日々考えディスカッションしながらものづくりをしています。

We are Hiring !

ARIGATOBANK では、今後も「お金で困っている人をゼロにする」ために様々な機能やサービスを創出していきたいと考えています。

今までにない未知のサービスを通して新しい未来を創る、そんなプロダクト開発に興味をもっていただけましたら、まずはカジュアルにお話できればと思いますので、お気軽に 採用ページ や Twitter の DM などからご連絡ください!

--

--