Android
想定読者:
Android アプリの開発を担当するモバイルエンジニア
iOS を主とし、Android 側の構成を把握したいモバイルエンジニア
本章では Wantedly の Android アプリのアーキテクチャを扱います。 ここでは Android ネイティブに閉じた話を対象とし、個別の実装やコードの詳細には踏み込まず、全体像を素早くつかめることを目的とします。
モバイルアプリの開発では、新しい技術の登場やプロダクトの課題の変化にあわせて、アーキテクチャは必然的に世代交代していきます。 全面的に作り直して一度きれいにしても、その効果は長続きせず、いずれまた段階的な改善を迫られます。
だからこそ、個々のアーキテクチャや実装そのものよりも、その選択の背後にある思想・戦略を理解することが重要です。 本章では Wantedly の Android アプリの構成を具体的に示しながら、なぜその形を選んでいるのかという考え方を中心にまとめます。
概要 — 設計思想
Wantedly の Android アプリは次の原則に基づいて設計されています。
段階的リニューアル: 全面刷新は行わず、画面・機能単位で少しずつ置き換える
単方向データフロー (UDF): 状態の流れを一方向に保ち、挙動を予測可能にする
Single Source of Truth: データベースを唯一の情報源とし、画面はそれを購読する
マルチモジュール: 役割と機能でモジュールを分割し、規模の拡大に耐える
ロジックの共有: ビジネスロジックを Kotlin Multiplatform (KMP) で iOS と共通化する
標準的な仕組みに寄せる: 独自実装より、AndroidX のような広く使われている標準やイディオムを優先する
移行を前提とした運用: 新規実装は新しい方式に寄せ、古い方式の新規追加を避ける
標準的な仕組みに寄せるのは、汎用的なスキルで開発でき、新しく参加する人にも学びやすく、AI による開発支援とも相性が良いためです。 標準に追随することで、将来の技術変化にも対応しやすくなります。
過去に iOS の全面リニューアルで開発が長期間止まった経験から、Android では大規模な一括変更を避け、段階的な改善を続ける方針を取っています。 iOS もこの全面リニューアルを経験しましたが、その後 SwiftUI のような新技術が登場したことで結局は段階的な改善が必要になり、リニューアルの効果は一時的なものにとどまりました。 この「段階的である」という制約が、以降で述べる多くの設計判断の背景にあります。
アーキテクチャ
全体構造
画面は Single Activity + Multiple Fragments で構成します。Activity はサインイン・オンボーディング用と、ログイン後用の 2 系統です。
1 つの画面(Fragment)に対して 1 つの ViewModel を配置します。
ViewModel はデータベースの変更を購読し、変更に応じて UI を更新します。
ViewModel が担う状態管理の実装は世代交代の途中で、現在は KMP の Reactor へ移行しています(詳細は後述の「状態管理の世代」)。
レイヤーと責務
上位から下位への単方向の依存で、次のレイヤーに分かれます。
UI: 画面の描画と入力の受け取り
ViewModel(Presentation): UI 状態の管理と、ユースケースの呼び出し(状態管理の実装の世代は後述)
UseCase: アプリ固有のビジネスルール
Repository: データソースの抽象化
DataSource: API やデータベースとの実際のやり取り
Entity: ドメインオブジェクトの定義
ViewModel は Repository を直接呼ばず、必ず UseCase を経由します。 ビジネスロジックを UseCase に閉じ込めることで、再利用しやすく、テスト時に差し替えやすくなります。
状態管理の世代
単方向データフローという思想は共通しつつ、それを実現する実装には新旧 2 つの世代があります。
旧
XML + Epoxy
Flux-like な ViewModel
RxJava + LiveData
新(最新)
Jetpack Compose
KMP の Reactor
Coroutines の StateFlow
どちらの世代も、画面のルートで状態を購読するという同じ作法に載せています。そのため画面単位で少しずつ移行できます。
新しく作る機能は、例外なく Compose と KMP の Reactor の組み合わせを採用しています。
KMP の Reactor は iOS とロジックを共有するための中心です。詳細は KMP の章で扱います。
画面遷移
画面遷移には Jetpack Navigation を用います。遷移の定義は機能ごとに分割して管理します。
モジュールをまたぐ遷移や、アプリ外部からの遷移を型安全に扱うため、独自の仕組みを用意しています。Jetpack Navigation は kotlinx.serialization のサポートにより型安全な遷移が可能になりましたが、そこへ移行する前のため、同等の仕組みを自前で構築しています。
画面の描画は Compose へ移行していますが、遷移の土台は Fragment ベースの Navigation を維持しています。
DI(依存性注入)
依存性注入は、オブジェクトが必要とする依存を外から渡す仕組みです。 結合度を下げて実装を差し替えやすくし、テストを書きやすくします。
依存性注入には Dagger Hilt を用います。
手書きのコンポーネント定義は持たず、Dagger Hilt が用意する標準のスコープ階層(アプリ全体・画面・ViewModel など)に素直に乗せます。
各機能モジュールが自分の依存グラフを自律的に組み立てるため、モジュールを増やしても DI の設定が一箇所に集中しません。
UI 実装の方針
UI は Jetpack Compose へ移行中です。新規の画面は Compose で実装し、新しい XML リソースの追加は避けます。
独自実装ではなく、Material Components をベースとしたデザインシステムのコンポーネントを優先して使います。デザインシステムの詳細はデザインシステムの章を参照してください。
既存の View ベースの画面も、修正の機会にあわせて Compose へ置き換えていきます。
モジュール構成
モジュールを分割する主な狙いは、ビルド時間の短縮(変更した箇所だけを再ビルド・並行ビルドできる)と、依存関係を単方向に保って全体の見通しを良くすることです。
数十のモジュールから成るマルチモジュール構成です。役割(レイヤー)による分割と、機能(feature)による分割を組み合わせ、規模の拡大に耐えられるようにしています。
依存には規律を設けています。同じグループ内のモジュール同士が横方向に依存することを禁止し、循環依存を防ぎます。たとえば、ある機能モジュールが別の機能モジュールへ直接依存することは避けます。
複数の機能で共通して使う UI モジュールは、データモデルに依存させません。UI 部品の再利用性を保つためです。
KMP の共有コードは、1 つのモジュールとして取り込みます。KMP 自体は内部的にマルチモジュールですが、iOS からは 1 つのまとまったモジュールとして扱われる性質があるため、Android でも同様に単一のモジュールとして依存します。
ビルド設定・DI・Compose の導入といった横断的な設定は、共通のビルド規約(convention plugin)にまとめて各モジュールへ適用します。設定の再利用性を高め、モジュールごとの build 設定を最小限に抑えるためです。
テスト戦略
テストはテスティングピラミッドの考え方に沿って構成します。 実行が速く安定したユニットテストを土台として厚く積み、実行コストが高く壊れやすい UI テストは上位に絞ります。 フィードバックの速さと安定性を保つためです。
ユニットテスト(土台): JVM 上で実行します。Android 依存が必要な箇所は Robolectric を用います。
UI テスト(上位): 実機で実行します。
モック: MockK を用います。
テストの命名は Given-When-Then の形式にそろえます。
歴史
Android の実装は、おおまかに次の順で世代を重ねてきました。
Java + XML による MVC
Kotlin + Flux-like な ViewModel(単方向データフローの導入)
KMP の Reactor(iOS とのロジック共有)
Jetpack Compose(宣言的 UI への移行)
詳細な世代の遷移は モバイルネイティブアプリについて の章に譲ります。
話を聞きに行きたい
Slack: #mobile_chapter
もっと知りたい
KMP の章(本 Handbook 内。ロジック共有と Reactor の詳細)
モバイルネイティブアプリについて の章(本 Handbook 内。各 OS の世代の全体像)
wantedly/visit-app リポジトリ (internal。Android のアーキテクチャや Compose の実装規約を含む)
最終更新