Now in REALITY Tech #94 Jetpack Compose導入の取り組みについての思いとパフォーマンスを考慮した実装の話
あけましておめでとうございます。REALITYのAndroidエンジニアのチームでマネージャーをしているメタルおじさんです。年始の初詣で神田明神に行ってIT情報安全守護の御守りをゲットしたので、今年も安全です(?)。
REALITYでのCompose化の取り組み
過去の記事でも何度か取り上げているように、REALITYではAndroidアプリのUIの実装のCompose化を進めています。昔から存在する機能はXMLとData Binding・View Bindingを使って作られているところも残っていますが、新規に作る機能についてはなるべくJetpack Composeを使って実装していくようにしています。
もちろん、常に一画面をまるまる新規で追加することばかりではありません。むしろ既存の画面に機能を追加する開発の方が多いです。その場合でも元から存在するXML・Data Binding・View Bindingに継ぎ足して済ませるだけでなく、周辺機能まで含めてComposeで実装し直すリファクタリングを行ったりしてComposeの実装範囲を広げていくようにしています。
既存の機能のUI実装をXMLからComposeに置き換える行為にはリスクがあります。たとえばリファクタリングの際にめったに使われない機能を見落としてしまって既存の機能が動作しなくなる不具合(デグレ)を起こす可能性があります。そのため、リスクを恐れて不用意なリファクタリングは避けるという方針を取るチームもあるでしょう。
しかしその一方でリファクタリングにはメリットもあります。その一つは新しい開発メンバーが既存の機能の詳細を理解するための勉強になるということです。コードを読むだけでなく「機能を壊さないようにより書き換える」ことによってコードへの理解が深まります。また、コードをレビューする側のメンバーにとっても、その機能の詳細を知るきっかけとなります。
特に、当時の実装担当者がすでにチームを離れてしまっている場合は、その機能に関して詳しいメンバーがいないという状態が起こります。コードとして表現されていない実装担当者の頭の中だけに存在していた知識は、メンバーの入れ替わりと共に失われます。すると、今後の機能開発で該当箇所を触ろうと思った時に既存仕様の見落としが起きたりして不具合の原因になります。
本当に今後の変更が一切行われないことが確定しているコードベースは別として、ちょっとでもなんらかの修正で手を入れる可能性があるコードについては現在いるチームメンバーの誰かしらが中身を理解しておく必要があるでしょう。そのためには、XMLからComposeへの移行のような積極的なリファクタリングに挑戦する価値はあると思います。
このような考えに基づき、リスクとメリットを天秤にかけて、メリットの方が勝っていると判断できる箇所については積極的にComposeへの置き換えを進めることにしています。
そして、このような姿勢でCompose化に取り組み続けた結果、この記事を書いている時点でREALITY Androidの画面の8割程度がComposeによる実装に置き換わりました。REALITYの開発が始まったのが2018年、Composeを本格的に導入し始めたのが2022年だったので、ここからさらに新規で作られた画面を除いたとしても古い機能の半分以上はComposeによりリプレースされていると言えます。
Compose化を進めたその先
前置きが長くなりましたが、ここまでREALITYへのComposeの導入に力を注いだ結果、現在ではAndroidエンジニア全員がComposeを使いこなし、作りたい機能が作れるようになりました。しかしここで満足していてはいけません。
この次は「より高品質なもの」を作れるようになるようにならなければいけません。
「より高品質なもの」とは何でしょうか?いくつか観点がありますが、そのうちの一つはパフォーマンスです。Composeは基本的にはパフォーマンスに優れた動作をしてくれますが、それでも書き方が良くないと無駄な計算処理が増えてしまったりスクロールがカクついたりするようになってしまいます。
Composeでパフォーマンスの良い実装をするためには、Composeがどのように動作するかを理解し、その動作に適したコードを書く必要があります。
これは単に「動くもの」を作るよりずっと難しいことですが、挑戦する価値はあります。
パフォーマンスの良い実装をするには
Composeに限らないことですが、パフォーマンスを良くするための基本的な考え方は、無駄をなくすです。では、Composeではどのような無駄が発生してしまうのでしょうか?
例えば以下のような原因が考えられます。
無駄なオーバードロー
無駄な再コンポーズ
今回はこのうち、オーバードローに注目してみます。
オーバードローとは
画面を描画する処理の中で、画面上の単一ピクセルを複数回描画することをオーバードローと言います。
たとえば、あるComposableが赤い四角を描画したとします。そしてその上に画面全体を赤く塗った後に画面全体を青く塗ったとすると、最終的な表示は青くなりますが、描画処理は「赤く塗る」「青く塗る」の2つが実行されています。この場合、「赤く塗る」の描画処理が無駄になっています。
ここまで極端な例はあまりないのではないかと思いがちですが、REALITYでは以下のようなオーバードロー箇所がありました。
ScaffoldのmodifierにModifier.backgroundを使って背景色を指定している
Scaffold Composable関数には、背景色を指定するための backgroundColor という引数があります。しかし、これを使わず、Modifier.background を使って背景色を指定しているコードが散見されていました。
Scaffold(
modifier = Modifier.background(designSystemColor.background),
) {
...
}
このようなコードになっていると内部的には「Scaffoldの背景色描画」と「Modifierによる背景色描画」がそれぞれ実行されることになります。そして最終的なScaffoldの背景色はModifierによる背景色描画により完全に上書きされてしまうので、Scaffoldの背景色描画処理がまるまる無駄になります。
Scaffoldには背景色を指定するためのパラメーターがあるので、こちらを使用するように修正しました。Modifier.backgroundの指定は削除します。
Scaffold(
backgroundColor = designSystemColor.background,
) {
...
}
UIコンポーネントごとに背景色を描画している
RecyclerViewのセルなど、小さいUIコンポーネントごとに個別に背景色を描画しているところがありました。
背景色はどれもデザインシステムで定義された「アプリ全体の共通の背景色」であったため、UIコンポーネントが配置される先の画面もまた同じ色の背景色を使っていました。
こうなっていると、親のComposable関数が描画した背景色の上にUIコンポーネントが全く同じ色を再度塗りつぶすことになり、無駄な描画処理が発生します。
このようなオーバードローをなくすために、UIコンポーネント側から背景色の描画(Modifier.background)を外しました。
CardやSurfaceには注意
MaterialデザインのコンポーネントであるCardやSurfaceなどは、これ自体が背景色を描画するようになっているため、意図せずオーバードローを作り込みやすいです。角丸の形状を作りたいだけだったら、Cardを使うよりもModifier.clip(RoundedCornerShape(…))を直接使う方が余計な背景色描画を加えずに済むので描画パフォーマンス観点で良いと思います。
Androidのドキュメントも読もう
オーバードローについての説明やよくある原因と対策、デバッグ方法など細かく紹介されているので、Androidのドキュメントも読んでみることをおすすめします。
おわりに
REALITY AndroidアプリはこれからもCompose導入を進め、さらに優れたパフォーマンスのアプリを目指して日々進化を続けていきます。今後のアップデートにご期待ください!