Engineering
Glossvation Engineering
Web・モバイル

Android アプリ設計の基本原則 — MVVM パッケージ構成と、現代 Jetpack への道筋

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

Glossvation ·

Android パッケージ構成

なぜ Android に 「設計」 が必要なのか

Android アプリは一見 Activity 中心の素朴なアプリに見えますが、実際には以下の複雑さを抱えています:

  • ライフサイクル (Activity/Fragment 再生成) に起因する状態喪失
  • 非同期処理 (API 呼び出し、DB アクセス) のタイミング管理
  • 画面サイズ・OS バージョン・メモリ環境の多様性
  • バックグラウンドからの復帰時のメモリ復元

なんとなく書いた Activity はすぐに God Object 化 します。View と Model を直接結ぶのではなく、間に ViewModel を挟む MVVM が Android 公式のアーキテクチャガイドにもなっているのはこのためです。

層の責務

層役割
View (Activity / Fragment / Composable)UI の描画と、ユーザー操作を ViewModel に通知
ViewModelView のためのデータ保持 + ユースケース呼び出し (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 を推奨。

LiveDataStateFlow
Null 許容はいいいえ (推奨)
初期値無し必須
Compose との相性observeAsState で可collectAsStateWithLifecycle でネイティブ
Coroutine との統合弱い強い
テスト容易性InstantTaskExecutorRule 必要自然

Jetpack Compose + Coroutines の組み合わせでは StateFlow の方が圧倒的に扱いやすいです。

2017 → 現代 (2024+) の変化

2017 当時現代の推奨
JavaKotlin (公式推奨、null 安全)
XML レイアウト + Data BindingJetpack Compose (宣言的 UI)
Dagger 2 (手書き Component)Hilt (アノテーション主体)
LiveData + ViewModelStateFlow + ViewModel
Retrofit + RxJavaRetrofit + Coroutines (suspend fun 対応)
SQLite ヘルパRoom (型安全な ORM)
AsyncTask / ThreadCoroutines + viewModelScope
AlarmManager / JobSchedulerWorkManager (遅延実行の統一 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 でスタートが現代的です。

関連記事