はじめに
こんにちは、ZOZOTOWN開発2部Androidブロックの小林(@kako_351)です。普段はZOZOTOWN Androidアプリの開発を担当しています。今年の3月に入社して機能改修や既存機能の調査などの業務に携わってきました。その中でZOZOTOWN Androidアプリについて知見を持っていないため、調査や開発の際に学習コストがかかるといった課題が見えてきました。本記事ではAndroidアプリの実装を把握するアプローチをご紹介します。
目次
- はじめに
- 目次
- 背景
- 実装を把握するアプローチの全体像
- ドキュメントの把握
- モジュール構成や画面遷移などの全体構造の把握
- アーキテクチャの把握
- ライブラリや使用技術の把握
- ビルドやデプロイなどCI/CD環境の把握
- テストの把握
- 実際にコードを読む
- チームメンバーとのコミュニケーション
- まとめ
背景
初めて触れるプロダクトへの知見が全くない状況で、調査や開発時に学習コストがかかっていました。そのため既存のメンバーよりもどうしても調査や開発に時間がかかっていました。ZOZOTOWN Androidアプリの規模の大きさから、調査や開発の度に実装を1から読んでいくと毎回学習コストがかかってしまうため、全体や方針を把握してこの学習コストを減らせないか考えました。
実装を把握するアプローチの全体像
基本的なアプローチとして、プロジェクトの大枠から詳細へと理解を深めていきます。
- ドキュメントの把握
- モジュール構成や画面遷移などの全体構造の把握
- アーキテクチャの把握
- ライブラリや使用技術の把握
- ビルドやデプロイなどCI/CD環境の把握
- テストの把握
- コードを読む
- チームメンバーとのコミュニケーション
ドキュメントの把握
目的
開発時に参考となるドキュメントが存在している場合があります。これらのドキュメントを把握することで、開発時に必要な情報を得ることができます。
アプローチ
ドキュメントをキャッチアップします。ZOZOTOWN Androidチームでは以下のようなドキュメントがまとめられています。
- アーキテクチャの説明
- コーディング規約
- ライブラリ
- ブランチ運用、GitHub運用
- ライセンス管理
- リリースフロー
- Android Studioの設定
- 開発環境
さらに、オンボーディングの一環でこれらのドキュメントがメンターから共有されます。入社したばかりのタイミングではどこに情報があるのかわからないので、このサポートは非常に嬉しいです。
モジュール構成や画面遷移などの全体構造の把握
目的
依存関係やファイルの配置場所など、どのような実装がどこに配置されているのかを把握します。また、自分が開発する際に何をどこに配置するべきかを理解します。
モジュール構成
最近のAndroidアプリは、マルチモジュール化していることが多いのでモジュール構成を把握します。
プロジェクトにモジュールの依存関係グラフを生成するGradleタスクがある場合はそれを活用できます。例えば、projectDependencyGraphのようなタスクがあります。ZOZOTOWN AndroidアプリにもprojectDependencyGraphが存在しているので、依存関係グラフを生成してみました。
この図はおおまかな構成の予測を立てるために活用します。楕円形がモジュールを表し、矢印が依存関係を表しています。例えば、「:app」モジュールから複数の矢印が伸びていて、その先を見ると「:feature:hoge」や「:feature:fuga」というモジュールがあります。このことから、機能単位でモジュールを作成していることが読み取れます。また、複数の「:feature」モジュールから「:common」というモジュールに矢印が伸びています。これは「:common」という名前と複数のモジュールから矢印が伸びていることから、共通部品を管理しているモジュールであることが読み取れます。
このようにモジュール名と依存関係から情報が読み取れます。例えばZOZOTOWN Androidアプリでは以下のような情報が読み取れました。
- featureモジュールで各機能単位をモジュール化している
- 共通部品はcommonモジュールで管理している
- ModelやRepositoryなどのデータレイヤーはdataモジュールに集約させている
- モジュール名から推測する役割に被りがあるのでプロダクトの成長と共にモジュール構成の見直しが行われている
モジュール構成を簡略化した図としては次のようになります。
かなり簡略化しましたが、このような構成であることがわかりました。
画面遷移
画面遷移をどのように管理しているのか把握します。いくつか考えられる候補があります。
- Jetpack Navigation
- FragmentManager
- Navigation Compose
Fragmentベースであれば、Jetpack NavigationやFragmentManagerを用いた画面遷移が考えられます。一方、フルComposeのアプリならNavigation Composeが候補に挙がってきます。
ZOZOTOWN AndroidアプリはFragmentベースで、Navigation Graphは存在しない構成です。よってFragmentManagerで画面遷移を実現している事が分かります。
モジュール間を跨ぐ画面遷移をどのように実現しているか把握します。ZOZOTOWN AndroidアプリではEventBusを利用してappモジュールを仲介する形で画面遷移を実現しています。
例えば、featureAモジュール内のFragmentAからfeatureBモジュール内のFragmentBに遷移する場合、featureAモジュールからFragmentBは直接参照できません。ZOZOTOWN AndroidアプリではEventBusを使いappモジュールのMainActivityにコールバックして、FragmentAからFragmentBへ画面遷移しています。図にすると以下のようなイメージです。
アーキテクチャの把握
目的
保守性や変更性、開発生産性のためにアーキテクチャを把握します。
アプローチ
ドキュメントが存在している場合はその内容をキャッチアップします。ただし、プロダクトで採用するアーキテクチャにも変化がありえます。アーキテクチャの候補としては以下のようなものがあります。
- MVVM
- MVP
- MVI
- Flux
AndroidアプリではAndroid公式のアーキテクチャガイドが存在しており、これを参考にしている場合があります。
ZOZOTOWN Androidアプリはアーキテクチャに関するドキュメントが存在しています。また、ZOZOTOWN Androidチームにはアーキテクチャについて議論するアーキテクチャ座談会という取り組みがあります。その議事録からも現在の方針を把握できます。
ZOZOTOWN Androidアプリはその歴史の長さや規模感から、画面や機能によってアーキテクチャが異なります。現在の方針は、MVVMに近い形が採用されているようでした。ただし、一部の画面はRedux1で実装されているなど、画面や機能によって異なるアーキテクチャが採用されていることがわかりました。
ライブラリや使用技術の把握
目的
使用しているライブラリや技術を把握することで既存実装を理解します。
アプローチ
ドキュメントとGradleを見ることで把握していきます。最近であればversion catalogでライブラリ管理している場合があるので、TOMLファイルなどを参照すれば理解が早いかもしれません。
ZOZOTOWN Androidアプリでもversion catalogを利用しています。一例ですが、具体的には次のようなライブラリを把握できました。
- UI系
- Jetpack Compose
- Epoxy
- API通信
- Retrofit2
- OkHttp
- Gson
- 非同期処理
- Kotlin Coroutines
- RxJava 2
- 画像系
- Picasso
- Coil
- DB
- greenDAO
- Room
- テスト
- MockK
- JUnit
- Robolectric
- Truth
- その他
- Dagger Hilt
- Firebase
また、CoroutinesやComposeはBOMで管理しています。具体的には以下のような記述からBOM管理であることがわかります。
[versions] compose-bom = "2024.01.00" [libraries] compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } compose-ui = { module = "androidx.compose.ui:ui" } compose-foundation = { module = "androidx.compose.foundation:foundation" } # ...
ビルドやデプロイなどCI/CD環境の把握
目的
テストやリリースフロー、Lintチェック、ライセンスチェックなどCI/CDで自動化している場合があります。それらを把握し、チームの開発・運用を理解します。
アプローチ
Pull Requestを確認することでジョブと実行環境が確認できます。ZOZOTOWN AndroidアプリではGitHub Actionsを採用しています。
もしくは、プロジェクトのフォルダやファイルを確認することでどのようなジョブが設定されているか確認できます。例えば、GitHub Actionsでktlintによる静的解析を自動チェックしている環境であれば以下のようなフォルダ、ファイルが存在していると思います。
.github ∟ workflows ∟ ktlint.yaml
リリース版アプリのビルドからGoogle Play Consoleへのアップロードも自動化している場合があります。ZOZOTOWN AndroidアプリでもビルドからリリースまでのフローはCI/CDで自動化されていました。
テストの把握
目的
開発時に期待されているテストの内容を把握します。
アプローチ
ユニットテストを書く文化があるかやどこまで書いているか、また自動化されているかを把握します。自動化は前述のCI/CD環境の把握の際に、ユニットテストを自動化していることなどを把握できます。カバレッジを収集している場合は、カバレッジレポートをどのように活用しているかなども合わせて確認します。
どのようなテストを書くべきかを理解しておくと開発時にスムーズでいいかもしれません。例えば、以下のような方針やルールがあるかもしれません。
- 実装の詳細ではなく振る舞いをテストする
- テスト名は命名規則に従う
ZOZOTOWN Androidチームでは、テックリードがチーム向けにユニットテストについて説明した資料がありました。その資料や過去のPull Requestを拝見することでテストの内容を参考にできます。
それらを参考にZOZOTOWN Androidチームはテスト文化があることや、ユニットテストの自動化もされていることがわかりました。また一部のメンバー間ではTDD(テスト駆動開発)で開発を進めていることもわかりました。
実際にコードを読む
目的
アプリの機能や画面がどのように実装されているのかを理解し詳細を把握します。
アプローチ
具体的なタスクを持つとモチベーションになるので、調査や開発タスクを進める過程でコードを読んでいくと理解が進みやすいと思います。そのようなタスクがない場合、機能単位でコードを見ていくとよいと思います。
ここではZOZOTOWN Androidアプリの商品詳細画面を例に挙げ、コードを読んでいく過程を紹介します。
コスメなど一部商品の場合、商品詳細画面に「衛生商品のため、返品・交換対象外です」といったメッセージを表示する以下のようなコンポーネントが存在します。
今回の例では、このコンポーネントがどのように表示されているかを特定していきます。
この記事で紹介するコードは、実際のZOZOTOWN Androidアプリのコードとは一部異なります。
コンポーネントの特定
まずはコンポーネントの特定です。以下のような方法があります。
- レイアウトファイルから辿る
- Layout Inspectorを使う
- Flipperなどのデバッガーツールを使う
ZOZOTOWN AndroidアプリはFragmentManagerで画面遷移をしていることがわかっているので、Fragmentのレイアウトファイルからコンポーネントを特定できます。ここでは、Fragmentから辿る方法とLayout Inspectorを使う方法を紹介します。
レイアウトファイルから辿る
まずはFragmentを特定します。Fragmentベースのアプリであれば以下のadbコマンドで現在表示しているFragmentを特定できます。
adb shell dumpsys activity top | grep 'Added Fragments' -A 1 # result # ... # Added Fragments: # #0: ItemDetailFragment{e53881d} (2e9ebbae-534f-4f02-90d1-dda3c302a8fb id=0x7f0a0147 tag=PACKAGE_NAME)
Fragmentが特定できたらそのFragmentのレイアウトファイルを見ていきます。レイアウトファイルからコンポーネントを特定できます。
Layout Inspectorを使う
Android StudioのLayout Inspectorでレイアウトをツリー構造で確認できます。ここから特定のコンポーネントを見つけることが可能です。
その他の方法
他にはFlipperなどのデバッガーツールを利用することでLayout Inspectorのようにレイアウトを確認できます。こちらはプロジェクトに導入済みであれば利用できますが、Layout Inspectorと同等の機能なのでこの記事では説明を割愛します。
詳細を読み解く
コンポーネントを特定できたら、UIレイヤーからデータレイヤー方向へコードを読み進めていきます。この時、これまで説明したアーキテクチャやライブラリを念頭におきながらコードを読んでいくと理解しやすいかと思います。
例えば、「衛生商品のため返品・交換対象外です」のメッセージがどのように表示されているかを調べるとします。
まずは特定したコンポーネントを見ていきます。この画面全体はFragmentですが、コンポーネントはJetpack Composeで実装されています。
@Composable fun ItemStatusInfo( viewData: ItemStatusInfoViewData, // ... ) { when (viewData) { is ItemStatusInfoViewData.Visible.NonReturnable -> { NonReturnableItemStatusItem( viewData = viewData.nonReturnableViewData, ) } // ... } }
ItemStatusInfoViewDataにより、表示するUIを分岐していることがわかりました。このItemStatusInfoViewDataの値がどのように確定されるのか見ていきます。
class ItemStatusInfoViewDataMapper { companion object { @JvmStatic fun fromDomainModel(itemStatusInfo: ItemStatusInfo) { if (itemStatusInfo.returnType != RETURN_TYPE_POSSIBLE /* 返品不可を表すType */) { return ItemStatusInfoViewData.Visible.NonReturnable(/* ... */) } // ... その他の分岐 } } }
ItemStatusInfoViewDataMapperでDomainModelのデータからItemStatusInfoViewDataの中身を決定しています。この先はデータの取得部分を読んでいきます。
事前のアーキテクチャの把握で、一部画面はReduxで実装されていることがわかっています。この商品詳細画面もReduxで実装されているので、その点を意識しながらコードを読んでいきます。
itemStatusInfoをどのように取得しているのかを読んでいきます。
package example.itemdetail.model data class ItemDetailState( // ... val itemStatusInfo: ItemStatusInfo, )
itemStatusInfoはItemDetailStateの中に存在しています。続いて、ItemDetailStateはReducerで作られているので中身を見ていきます。ReducerはActionに応じて新しいStateを生成する役割を持っています。
class ItemDetailReducer { suspend fun reduce(action: ItemDetailAction, state: ItemDetailState): ItemDetailState { return when (action) { is ItemDetailAction.GetItemDetailSucceeded -> { val itemDetail = action.itemDetail ItemDetailState(/* itemDetailを元にステートを更新して返す */) } } } }
ItemDetailActionが持つパラメータのitemDetailを元にItemDetailStateを作成しているのがわかりました。
次にitemDetailがどのように生成されるのか見ていきます。
ZOZOTOWN AndroidアプリにおけるReduxでは、APIリクエストなどの非同期処理はMiddlewareで行われています。以下のようなGetItemDetailMiddlewareが存在します。この中でItemDetailRepository.getItemDetailを経由してitemDetailを取得しています。
class GetItemDetailMiddleware( private val itemDetailRepository: ItemDetailRepository ) { fun dispatch(): Dispatcher<ItemDetailAction> : (Dispatcher<ItemDetailAction>) -> Dispatcher<ItemDetailAction> = { next -> return when(action) { is ItemDetailAction.GetItemDetail -> { val itemDetail = itemDetailRepository.getItemDetail(/* parameters */) ItemDetailAction.GetItemDetailSucceeded(itemDetail) } } } } class ItemDetailRepositoryImpl @Inject constructor( private val apiService: ItemDetailApiService, ): ItemDetailRepository { override suspend fun getItemDetail(/* parameters */) { val response = apiService.getItemDetail(/* parameters */) // ... 後続処理 } }
apiService.getItemDetailでAPIリクエストをどのように実装しているか見ていきます。API通信においてRetrofit2が利用されていることを既に知っているため、以下のコードを見てインタフェースを理解して終わりです。
interface ItemDetailApiService { @GET(/* endpoint */) suspend fun getItemDetail( /* query parameters */): Response<ItemDetailResponse> }
このようなアプローチで事前に把握した情報を合わせながら、コードを読んでいくと理解が進みやすいと思います。
チームメンバーとのコミュニケーション
目的
コードや資料のみではわからないことも存在します。そのため、メンバーとのコミュニケーションを通して実装がそうなっている理由や経緯を理解します。
アプローチ
Slackなどの社内チャットツールやメンター制度があればその場を活用するとよいと思います。質問する際には、参考にした情報、聞きたい内容を明確にすると適切な回答が得られやすいです。
ZOZOTOWN Androidチームには、開発に関する困りごとを気軽に相談できるSlackチャンネルが存在しています。そのSlackチャンネルで相談すると誰かしらがすぐに反応してコメントくれるので困る事が少なかったです。また、私がZOZOに入社したばかりの頃はメンターに口頭でも相談していました。
チーム側にこのようなフォロー体制があることで、JOINした側としては安心できました。
まとめ
本記事では大規模なAndroidアプリの実装を紐解いていくアプローチを紹介しました。構成や実装の解像度が上がったことで以前よりも学習コストを減らせたと思います。新しい環境になり既存のプロダクトへの知見がなく困っている方がいれば、ぜひ参考にしてみてください。
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。
- Reduxは元々JavaScriptのライブラリでStateを管理するためのフレームワークです。ZOZOTOWN AndroidアプリではReduxをカスタマイズして一部画面で導入されています。Reduxの詳しい説明はReduxの公式ウェブサイトを参考にしてください。↩