Android アプリ設計の基本原則 — MVVM パッケージ構成と、現代 Jetpack への道筋
Android アプリを複数人で開発するときに崩れない MVVM パッケージ構成の指針。当時の Dagger / LiveData / Data Binding の構造と、現代の Jetpack Compose / Hilt への乗り換え時の考え方。

なぜ Android に 「設計」 が必要なのか
Android アプリは一見 Activity 中心の素朴なアプリに見えますが、実際には以下の複雑さを抱えています:
- ライフサイクル (Activity/Fragment 再生成) に起因する状態喪失
- 非同期処理 (API 呼び出し、DB アクセス) のタイミング管理
- 画面サイズ・OS バージョン・メモリ環境の多様性
- バックグラウンドからの復帰時のメモリ復元
なんとなく書いた Activity はすぐに God Object 化 します。View と Model を直接結ぶのではなく、間に ViewModel を挟む MVVM が Android 公式のアーキテクチャガイドにもなっているのはこのためです。
層の責務
| 層 | 役割 |
|---|---|
| View (Activity / Fragment / Composable) | UI の描画と、ユーザー操作を ViewModel に通知 |
| ViewModel | View のためのデータ保持 + ユースケース呼び出し (Android のライフサイクルから独立) |
| Model (UseCase / Repository / Entity) | ドメインロジック + データ永続化 |
原則: View は ViewModel を知る、ViewModel は Model を知る、 逆方向の参照は無い (= Model から View を触ったら設計失敗)。
パッケージ構成の指針
com.example.app/
├── App.kt # Application クラス
├── di/
│ ├── module/ # DI module (Hilt なら @InstallIn)
│ └── component/ # (Dagger 時代のみ、Hilt なら不要)
├── domain/
│ ├── model/ # ドメインモデル (data class)
│ ├── repository/ # Repository interface
│ ├── usecase/ # ユースケースクラス
│ └── event/ # ドメインイベント
├── data/
│ ├── remote/ # API クライアント (Retrofit 等)
│ ├── local/ # Room DAO 等
│ └── repository/ # Repository impl
├── presentation/
│ ├── activity/
│ ├── fragment/
│ ├── adapter/ # RecyclerView.Adapter
│ ├── composable/ # (Compose 採用時) UI コンポーネント
│ ├── viewmodel/
│ └── ui/ # カスタムビュー、ダイアログ
├── infra/ # OS 機能ラッパ (Storage, Alarm, Location)
└── exception/
実務では domain/ と data/ を分ける のが肝。
「Repository interface は domain 側、実装は data 側」 という構造にすると、
UI テストのときに Fake Repository 差し替えが簡単になります。
各層の責務
di/ — 依存注入 (DI) モジュール。
2017 当時は Dagger 2 が主流でしたが、現代は Hilt (Jetpack Hilt) 一択 です。
Hilt は Dagger ベースで Android 向けの決まり文句を自動化してくれます。
domain/ — ドメイン層。
UI フレームワークに依存せず、純粋な Kotlin コードのみ。
UseCase は suspend operator fun invoke() 形式で書くと呼び出し側が綺麗。
data/ — データ層。
Repository の実装、Retrofit の interface、Room の @Dao などを配置。
presentation/ — View + ViewModel。
presentation を ui と呼ぶチームもあります。
ViewModel は androidx.lifecycle.ViewModel を継承、
状態は StateFlow / LiveData で View に流します。
infra/ — OS 機能のラッパ。
AlarmManager、Location、WorkManager などを薄くラップして、
テスト時にモック化しやすくします。
状態管理: LiveData → StateFlow
2017 当時は LiveData 一択でしたが、現代は StateFlow / SharedFlow を推奨。
| LiveData | StateFlow | |
|---|---|---|
| Null 許容 | はい | いいえ (推奨) |
| 初期値 | 無し | 必須 |
| Compose との相性 | observeAsState で可 | collectAsStateWithLifecycle でネイティブ |
| Coroutine との統合 | 弱い | 強い |
| テスト容易性 | InstantTaskExecutorRule 必要 | 自然 |
Jetpack Compose + Coroutines の組み合わせでは StateFlow の方が圧倒的に扱いやすいです。
2017 → 現代 (2024+) の変化
| 2017 当時 | 現代の推奨 |
|---|---|
| Java | Kotlin (公式推奨、null 安全) |
| XML レイアウト + Data Binding | Jetpack Compose (宣言的 UI) |
| Dagger 2 (手書き Component) | Hilt (アノテーション主体) |
| LiveData + ViewModel | StateFlow + ViewModel |
| Retrofit + RxJava | Retrofit + Coroutines (suspend fun 対応) |
| SQLite ヘルパ | Room (型安全な ORM) |
| AsyncTask / Thread | Coroutines + viewModelScope |
AlarmManager / JobScheduler | WorkManager (遅延実行の統一 API) |
落とし穴
1. ViewModel から Context を参照して Activity をリーク
ViewModel が Context を保持すると Activity 再生成後にリーク確定。
Application Context が必要なら Hilt の @ApplicationContext を注入します。
2. 画面回転でデータが消える
状態を ViewModel で保持すれば回転で消えないのが基本ですが、
プロセスキル後の復帰には SavedStateHandle が必要:
class SearchViewModel @Inject constructor(
private val savedState: SavedStateHandle
) : ViewModel() {
var query: String
get() = savedState["q"] ?: ""
set(value) { savedState["q"] = value }
}
3. Fragment の onViewCreated で StateFlow を collect する
ライフサイクルを超えて collect し続けるとメモリリーク。
必ず viewLifecycleOwner.lifecycleScope を使う:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { render(it) }
}
}
4. 1 画面 1 Activity に固執する
Single Activity + Navigation Component で全画面を Fragment / Composable で表現する方が、 ディープリンク・遷移管理・Back Stack の制御が楽です。
まとめ: Android のパッケージ構成は MVVM + 層分離 を基本に、
domain / data / presentation の 3 分割で破綻を防ぐのが鉄則。
2017 当時の技術スタック (XML + Dagger + LiveData) は今も読めますが、
新規プロジェクトは Compose + Hilt + StateFlow でスタートが現代的です。
関連記事
- Web・モバイル·
SOAP / REST / gRPC の選び方 — 内部通信とエッジ API で違う
3 つの API プロトコルの歴史的背景、通信の重さ、スキーマ定義、エコシステム、適材適所。社内マイクロサービス間と外部公開 API での選定基準を整理。
- Web・モバイル·
Spring Bean ライフサイクル — スコープ・初期化順・落とし穴
BeanDefinition のロードから @PostConstruct まで、Spring Bean がどの順序で生成されるかを 7 ステップで整理。Singleton/Prototype/Request/Session スコープの違いと実務的な使い分けも。
- Web・モバイル·
Spring Boot プロジェクト構成 — 10 人規模で崩れないパッケージ設計
Controller / Service / Repository の責務境界、ドメイン層の切り方、テストの並べ方まで。Spring Boot API 開発で「後から直せなくなるコード」を防ぐためのパッケージ構成の指針。