高橋啓太氏(以下、高橋):「ZOZOTOWN AndroidへのJetpack Composeの導入」ということで、発表します。まず自己紹介ですが、株式会社ZOZOテクノロジーズ(※取材当時)で「ZOZOTOWN」のAndroidエンジニアをやっています、高橋といいます。2020年の新卒入社で、今年で2年目です。

簡単に、ZOZOTOWNについて紹介させてください。ZOZOTOWNは、日本最大級のファッション通販サイトで、2021年の3月にはコスメ専門モールの「ZOZOCOSME」がオープンしました。

さっそく、今回のテーマとなるJetpack Composeについて簡単に説明していきます。Jetpack Composeとは、宣言的UIのツールキットで、Kotlinのコード上でUIの実装が可能になります。また、XMLを用いた既存のレイアウトとも相互運用可能となっています。つい最近、1.0.0のstableがリリースされました。(※取材当時)

Jetpack Composeでは、Composableアノテーションを付与した関数でUIを定義します。

そんなJetpack Composeについて、ZOZOTOWN Androidへの導入時に行った作業を順に説明していきます。

導入のステップは、大きく分けて3つあります。1つ目は、開発環境の整備。2つ目は、Compose導入時の課題の洗い出し。3つ目は、その課題の解決です。

まず、1つ目の開発環境の整備について説明します。Composeを使用した開発の前に、Android Studio Arctic Foxで既存プロジェクトのビルドを行いました。Android Studio Arctic Foxでは、Jetpack Composeの開発のサポートが強化され、Compose UIのプレビューなどが実装されました。

また、ZOZOTOWN Androidでは、Arctic Foxの導入と同時に、Android Gradle プラグイン 7.0.0へのアップデートと、JDK 11の導入を行いました。さらに、Dagger Hiltのアップデートも行いました。この環境で、一度ZOZOTOWN Androidをビルドし、問題なくビルドできることを確認しました。

Jetpack Compose beta08からはKotlin 1.5が必要となるため、Kotlinのアップデートもしました。アップデートの実施に際しては、リリースノートなどのドキュメントの確認とZOZOTOWN Androidへの影響箇所の調査を行いました。

ドキュメントの確認は、チームメンバーで読み合わせるかたちで行いました。のちほど登場するのですが、Composable内でFlowを利用するために、androidx.lifecycleのアップデートも行いました。

次のステップでは、Compose導入時の課題の洗い出しを行いました。課題を洗い出すため、Composeを使ったプロトタイプ実装とレビューを繰り返し行いました。プロトタイプ実装とレビューによって、大きく2つの課題が明らかになりました。

1つ目が、既存のZOZOTOWN AndroidのUI状態管理方法が、Composeに適していないということ。そして2つ目が、無秩序なComposable作成による、Composableの再利用性・可読性の低下です。

では、その2つの課題の解決に向けて、それぞれどのようなアプローチを取ったのかを説明していきます。

1つ目の課題は、既存のZOZOTOWN AndroidのUI状態管理方法が、Composeに適していないというものですが、この課題の解決に向けて、まずComposeが採用されているプロジェクトの実装を調査しました。そしてそれをもとに、Composeに適した設計を検討しました。

Composeは、公式のページにもあるように、UIの差分更新を自動で行います。なので、開発者は差分を意識してUIにデータを通知する必要がありません。

ここで、既存のZOZOTOWN Androidのうち、今回問題となった部分を説明していきます。ZOZOTOWN Androidでは、アプリケーションの状態に変更があると、ViewModelはその更新差分をFragmentに対して通知します。Fragmentはその内容をもとに、更新が必要なViewの更新を明示的に行います。

プロトタイプ実装とレビューによって、この更新差分を通知する部分が、Composeには適していないということが今回わかりました。

この問題についてコードレベルで説明します。ZOZOTOWNのUIの状態管理には、主に2つのオブジェクトが登場します。1つ目が、ViewDataです。ViewDataは、UIに表示するデータをまとめたデータクラスで、カスタムビュー単位で作成します。

2つ目が、ViewStateです。ViewStateはUIの状態を表すsealed classで、画面単位で作成します。ViewStateはそれぞれ、ViewDataを保持します。ZOZOTOWN Androidでは、ViewStateの切り方について明確なルールがこれまで存在しませんでした。

これらのViewData、ViewStateは、ViewModelの中でこのように扱われます。上から説明していこうと思います。

まずViewModelには、Activity/FragmentにViewStateを通知するFlowまたはLiveDataが定義されています。次に、そのデータの更新と、ViewStateを先ほどのFlowやLiveDataに流すメソッドが定義されています。そして最後に、ViewStateが定義されています。

それぞれのViewStateでは、更新差分のあるViewDataのみを保持しています。これがComposeでは扱いづらいということがわかりました。

先ほども紹介したように、Composeでは、差分を意識してUIにデータを通知する必要がありません。裏を返せば、これを活用するためには、画面全体の状態を1つにまとめる必要があるということです。

その点を踏まえ、UIの状態管理方法を見直しました。さらに、Eventの処理方法についても見直しを行いました。

まず、UIの状態管理方法の見直しについてです。設計を検討するにあたって、事前に参考実装の調査を行いました。そのうちの1つが、tiviというプロジェクトです。tiviは、Jetpack Composeで実装されたプロジェクトで、既存のZOZOTOWNと近いアーキテクチャであるため、今回の設計検討はtiviを参考にして行いました。

参考実装をもとに、まずは画面全体のViewDataを1つのViewStateに統合しました。そして、これまで差分のみを保持していたViewStateを削除しました。

次に、アプリケーションの状態を管理するFlowを用意しました。それらのFlowを、combine、そしてtransformすることで、アプリケーションの状態に変更があった時に、画面全体の状態を持った先ほどのViewStateを作成し、通知することができるようになりました。

そうすることで、Composable側では常に画面の全体の状態を受け取ることができ、ComposeのUIの差分更新を利用できるようになりました。

全体の流れは、このようになります。アプリケーションのStateが変化すると、ViewModelではそれを1つのViewStateにまとめ、Composableに通知します。

次に、Eventの処理方法についてです。ViewModel以下のEventの扱いに関しては、既存のZOZOTOWNと大きく変わる点はありませんでした。既存のZOZOTOWNにもあるのですが、ViewEventという、ユーザーインタラクションによって発生するEventを定義したsealed classを作成しました。ViewEventは、Flow/LiveData、LiveDataの場合はSingleLiveEventでViewEventを通知するようにしました。

ComposableからのViewEventの発行は、このようになりました。ViewEventは、各画面のトップレベルのComposableから発行するようにしました。そうすることで、下層のComposableは、ViewEventへの参照を持たず、ViewEventを意識しないかたちとなりました。これによって、下層のComposableの再利用性を上げることができました。

ここまで説明したViewState、ViewEventについて、ViewEventの発生から UIの更新までの流れはこのようになります。

画面上で、ユーザーインタラクションなどによってEventが発生すると、そのEventはLambdaを介して、上層のComposableへと伝搬されます。そして上層のComposableは、ViewModelに対して、ViewEventの発行を行います。

ViewModelは、ViewEventを受け取るとアプリケーションのStateを更新し、それらの状態を1つのViewStateというオブジェクトにまとめてComposableへと通知します。

ComposableはViewStateを受け取ると、UIの描画に必要なデータを下層のComposableに伝搬します。各Composableは、渡されたデータをもとに、UIの更新が必要だった場合に自動的に差分更新を行います。

このように、UIの状態管理、Eventの処理方法を見直し、1つ目の課題である、既存のZOZOTOWNのUI状態管理方法がComposeに適していないという問題を解決しました。

次に、2つ目の課題についてです。今後、チームでComposeを使用した開発を進めていくと、無秩序にComposableが作成され、Composableの再利用性・可読性が低下する懸念がありました。

そこで、ReactなどWebフロントの宣言的UIフレームワークではどのようにUIの要素を分割しているのか、事例を調査し、チームでの開発に備えたComposable設計ルールの検討を行いました。その調査の中で登場したのが、Atomic Designです。

Atomic Designは、主にWebフロントの世界で用いられるデザインシステムで、堅牢性の高いUI実装を実現できます。Atomic Designでは、コンポーネントと呼ばれるUIの要素を、Atoms、Molecules、Organisms、Templates、Pagesに分類し階層化します。

Atomic Designで定義されるコンポーネントの階層構造は、このようになっています。PagesはTemplatesへの依存を持ち、TemplatesはOrganismsへの依存を持ち、OrganismsはMoleculesやAtomsへの依存を持つというように、上層のコンポーネントが下層のコンポーネントへの参照を持ちます。

逆に下層のコンポーネントは、上層のコンポーネントに依存できません。これにより、下層のコンポーネントの変更による上層のコンポーネントへの影響を抑えることができます。

ZOZOTOWN Androidでは、このAtomic Designをベースに、Composable設計ルールを制定しました。それぞれのコンポーネントについて、ZOZOTOWN Androidでどのように定義したかを説明していきます。

まずAtomsですが、Atomsは機能的に分割できる最小のUI要素です。ZOZOTOWN Androidでは、Text、Button、Row、Columnなど、Composeで提供されているComposable、また吹き出しやアイコンなど、ZOZOTOWN Android独自に作成したComposableのうち、それ以上分割できないものをAtomsとしました。

命名規則というよりは方針なのですが、すべてのアプリケーションで利用できるように可能な限り抽象的な名前としています。

次に、Moleculesです。Moleculesは、ユーザーに対して目的・動機を提供する最小のUI要素です。ZOZOTOWN Androidでは、TextやRowなどのAtomsを組み合わせて作成したComposableです。

命名に関しては、ユーザーが何を実現したいのかを表す名前とします。右の例だと、これはドロップダウンメニューです。ドロップダウンメニューは、Moleculesとして定義しています。

Organisms。Organismsは、独立して成立するコンテンツです。ZOZOTOWN Androidでは、Atoms、Molecules、そしてほかのOrganismsを組み合わせて作成したComposableを、Organismsと定義しています。命名に関しては、そこに表示する情報自体を表しました。

そして、Templates。Templatesは、ページのコンテンツ構造を明確にするオブジェクトです。ZOZOTOWN Androidでは、画面全体のレイアウトを管理し、各UI要素に必要なViewDataを分配する役割を持ったComposableがTemplatesとなっています。Templatesは、1画面に対して1つ作成することとしました。

命名に関しては、頭に画面名がきて、サフィックスにScreenがくるかたちにしました。

そして、Pagesです。Pagesは、実際のデータを画面に反映するオブジェクトです。ZOZOTOWN Androidでは、ViewModelへの依存、つまり動的なデータへの関心を持つComposableをPagesとしています。

また、先ほどのViewEventのところでも出てきたように、ViewModelに対してViewEventを発行するComposableも、このPagesです。

また、1つ下のレイヤーにあるコンポーネントであるTemplatesに、実際のデータを提供する役割も、このPagesのComposableが担っています。

命名に関しては、Templatesと同じように、画面名そしてサフィックスにScreenとなります。引数が違うかたちです。

ZOZOTOWN Androidのデバッグメニューの例では、このようになっています。TextやButtonは、Atoms。SpinnerはMolecules。そして、それらを束ねたものが、Organisms。画面全体がTemplates、またPagesとなります。

以上のように、Composable設計ルールを制定することで、2つ目の課題である、無秩序なComposable作成による、Composableの再利用性の低下、また可読性の低下という問題を解決しました。

まとめです。まず、ZOZOTOWN AndroidへJetpack Composeを導入しました。Compose導入のための課題を洗い出し、それぞれ解決していきました。

1つ目は、UIの状態管理、Eventの処理方法の見直しです。そして2つ目が、Atomic DesignをベースにしたComposable設計ルールの制定です。

今後の課題は、いくつかあります。1つ目は、UIの状態管理方法のルール化です。今回検討したUIの状態管理方法は、まだルール化まで至っていません。今後はチームでよりスムーズに開発を行っていくため、今回の検討内容をルールに落とし込んでいきたいと考えています。

2つ目は、より複雑な画面でのStateの扱いです。今後は、より大きく複雑な画面でComposeを使用して開発していくことを検討しているため、UIの状態管理に関して、今回検討した設計ではカバーできないユースケースの登場が考えられます。そういったユースケースへの対応も考えていきたいと思っています。

3つ目は、Composable設計ルールのブラッシュアップです。今回制定したルールを、実際にチームで運用して、よりZOZOTOWN Androidに適したかたちに変えていこうと考えています。

最後に、デザイナー・エンジニア間のユビキタス言語としてのAtomic Designの活用です。Composableの設計だけでなく、デザイナーとエンジニア間の共通言語として、Atomic Designを活用し、デザインに関するコミュニケーションをよりスムーズにしていきたいと考えています。これらの課題についても、今回のプロトタイプ実装とレビューを繰り返して行ったように、実装と検証を繰り返して改善していこうと考えています。

以上です。ありがとうございました。