この記事は、ニフティグループ Advent Calendar 2022 23日目の記事です。
はじめに
会員システムグループの山田です。
ニフティでは昨年12月(iOS)と今年3月(Android)に、会員様向けアプリとしてマイ ニフティをリリースしました。
ニフティとしては久しぶりの新規アプリ開発となり、既存アプリのレガシー化も進んでいたことから、本アプリの開発ではゼロベースで技術スタックを見直し、現在のアプリ開発において標準的な技術に揃えることにしました。
その中でAndroidにおいてはUI構築にJetpack Composeを選定したので、その結果どうだったか、ということについてお話しします。
Jetpack Composeとは
2021年にバージョン1.0.0が公開された、Android用の新しい公式UIツールキットです。
Androidではその登場以来、XMLでUIを記述し、Java/Kotlinで操作するというスタイルでUIの構築が行われてきました。DatabindingやView Bindingなどの補完技術が登場しても、この基本は変わらず一貫していました。
Jetpack Composeはこれを覆す転換点となるツールキットです。主に以下のような特徴を持ちます。
- UIをすべてKotlinの関数により記述する
- 宣言的UIの採用
特にReactで一大ムーブメントを巻き起こした宣言的UIの採用が特徴的で、今までの「XMLに書いたものをコードから変化させる」スタイルから「UIをを予めすべて定義しておいて、データ状態に応じて自動的に変化する」スタイルに一変しています。
詳細についてはAndroid Developersに公式の解説がありますので、そちらもご参考ください。
よかったこと
記載するコードは簡略化のため、一部の属性値などを省略しています。
記述がシンプルになる
UIが非常にシンプルに書けるようになり、UI記述にかかる時間を大幅に削減することができています。
特に以下の点が効いています。
レイアウトのネストを気にする必要がない
上のようなレイアウトを組もうとした場合、従来の方法でシンプルに書くと以下のような構造になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<LinearLayout android:orientation="horizontal"> <ImageView /> <LinearLayout android:orientation="vertical"> <TextView /> <TextView /> </LinearLayout> </LinearLayout> |
しかしこれはよくないとされる記述です。
従来のAndroidのレイアウトではネストが深ければ深いほど、加速度的にレンダリング時間が増加するという問題が存在します。したがってこのような記述は避け、ConstraintLayoutをはじめとする複雑なレイアウト方法をとり、なるべくフラットに記述する必要がありました。これは学習負荷が高く、またサッと組むには時間のかかる方法です。
Jetpack Composeではネストの問題が解決されているため、このような考慮が不要です。
1 2 3 4 5 6 7 8 9 10 |
@Composable fun ArticleRow(article: Article) { Row { AsyncImage(model = article.imageUrl) Column { Text(article.title) Text(article.body) } } } |
Jetpack Composeではアノテーションを付けた関数(Composable関数)でUIを記述します。この中でRowとColumnが従来のLinearLayoutに対応しています。
このように見た目通りの構造を記述しても、レンダリング速度が大きく落ちることがありません。
リストもシンプルに書ける
従来、リスト形式のUIを作成する場合はRecyclerViewを使用することが多かったと思います。
RecyclerViewを使うためには面倒な準備が必要で、
- DiffUtilを使ってデータの同一性判定を定義
- RecyclerView.ViewHolderを継承してView保持クラスを定義
- RecyclerView.Adapterを継承してデータとのバインディングを定義
という手順を踏んでようやく使えるようになります。
Jetpack ComposeではLazyColumnを使えばよく、
1 2 3 4 5 6 7 8 9 10 11 |
@Composable fun ArticleList(articles: List<Article>) { LazyColumn { items( items = articles, key = { article -> article.id } ) { article -> ArticleRow(article) } } } |
このように簡単な記述でリストが記述できます。
プレビューが容易
従来のXMLでもプレビューはできるのですが、コードから動的に書き換えられる部分のプレビューが難しいという問題がありました。またレイアウトのネストを深くできないという制約上、UIを細かいコンポーネントに分割してプレビューすることもなかなか難しいということも課題でした。
Jetpack Composeのプレビューは非常に簡単です。UIコンポーネントが全て関数で記述されるので、プレビュー用のデータを引数に与えるだけでレンダリング可能な状態になります。あとは@Previewアノテーションを付与した関数でラップすればプレビューの完成です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Preview @Composable fun ArticleRowPreview() { MyTheme { ArticleRow( Article( id = "xxx", title = "タイトル", body = "本文", imageUrl = "https://example.com/image.png" ) ) } } |
複数パターンを試したければ引数を変えたものを別途用意するだけで済みます。細かい関数に分けていけばその単位でプレビューが可能なので、ReactなどにおけるStorybookのようなコンポーネントカタログとしての利用が可能です。
React Hooksに近い
Jetpack ComposeはReactのHooks APIに近い概念や文法を持ちます。
UIを関数で記述するという基本文法もそうですし、状態や副作用処理に関しても
1 2 3 |
const [state, setState] = useState(0); useEffect(() => { ... }, [state]) |
1 2 3 4 5 |
val state = remember { mutableStateOf(0) } LaunchedEffect(state) { ... } |
このように大まかな対応が取れます。
ニフティでは新人教育の中でReactを取り入れているため、アプリ開発にジョインする際の学習コストを抑えることができています。
状態管理と強依存しない
Jetpack Composeは専用に用意されたStateの仕組みによって状態を管理します。では全ての状態をStateで管理する必要があるのかというとそうではなく、RxJavaやLiveData、Flowといった従来の状態管理の仕組みと連携することが可能です。
例えばViewModelに保存されたFlow型変数を、以下のようにState型に変換できます。
1 |
val state = viewModel.flow.collectAsState() |
変換が用意されているため、UIはJetpack Composeで書きつつ、状態管理やデータアクセスは従来通り、Jetpack Composeに依存しない形で記述することが可能です。このため、UI以外のレイヤーは従来と同様の設計を行えばよいことになります。
マイ ニフティではモダン化を行った分、新規に採用するライブラリも多く、経験の少なさがバグや工数増大のリスクになり得ました。その点、中核となる設計に従来の知見を使えることはリスクを低く保つことに大きく貢献しました。
最新のAndroidが必要ない
マイ ニフティではiOS版でも同様に技術スタックの見直しを行ったのですが、SwiftUIの採用には至りませんでした。これは開発初期の時点でiOS 10を最低バージョンとしており、SwiftUIに非対応であったためです。便利な機能であってもそれがOSに対して実装される場合、採用可能になるまでは数年間待つ必要があります。
Jetpack ComposeはAndroidXと呼ばれる公式補助ライブラリの一部として開発されており、Android本体とは独立しています。Jetpack Compose 1.0.0の時点でAPI 21(Android 5.0)以上に対応しているため、問題なく採用できました。
おわりに
マイ ニフティでJetpack Composeを採用してよかった点を見てきました。特に記述のシンプルさは追加開発や新人のジョイン時にも大きく貢献しており、社内のアプリの中でも頻度の高いアップデートを行うことができています。
一方で開発していく中でイマイチだと感じる点もありました。次回はそちらについてご紹介する予定です。