Navigation 3 時代でも ViewModel は State Holder であるべきか?

Compose と Navigation 3 が変えた ViewModel の役割

長年、Android 開発では一つの考え方が当たり前とされてきました。

画面の状態(State)は ViewModel に持たせる。

これは単なるベストプラクティスではなく、Android アーキテクチャの基本原則の一つでした。

ViewModel は Configuration Changes(画面回転など)でも破棄されず、さらに SavedStateHandle を使えば Process Death 後の状態復元もできます。

そのおかげで Activity や Fragment は UI を表示することに専念でき、画面の状態はすべて ViewModel が管理する、という設計が一般的になりました。

しかしここ数年で、この考え方は少しずつ変わり始めています。

Compose も Navigation 3 も ViewModel を不要にしたわけではありません。

代わりに、それぞれが 「本来その State を持つべき場所」 を用意するようになりました。

そこで、こんな疑問が生まれます。

Navigation 3 の時代でも、ViewModel は State Holder であるべきなのでしょうか?

 

🤔 以前の ViewModel は画面の State をすべて抱えていた

Compose が登場する以前は、多くの ViewModel が画面に関するほぼすべての状態を保持していました。


ViewModel
├── UI State
│   ├── text
│   ├── scrollPosition
│   ├── selectedTab
│   └── dialogState
│
├── Navigation State
│   └── screen arguments
│
└── Presentation Logic

この構成には多くのメリットがありました。

- Configuration Changes に強い
- Process Death 後も状態を復元できる
- Activity / Fragment をシンプルに保てる
- UI とビジネスロジックを分離できる

そのため、新しい State が増えたら、

とりあえず ViewModel に置く

という設計が自然になっていました。

 

🤔 Compose は UI State を UI に戻した

Jetpack Compose は、この考え方を少し変えました。

すべての UI State を ViewModel に持たせる必要はなく、Composable 自身が State を持てるようになったのです。

例えば検索文字列なら、


var query by remember {
    mutableStateOf("")
}

だけで十分です。

さらに Configuration Changes や Process Death を越えて保持したいなら、


var query by rememberSaveable {
    mutableStateOf("")
}

を使えます。

例えば次のような State は、

- TextField の入力内容
- 選択中のタブ
- 展開状態
- ダイアログの表示状態
- スクロール位置

どれも 純粋な UI State です。

ビジネスロジックではありません。

そのため、実際に使っている Composable の中に置く方が責務も分かりやすくなります。

Compose はまず、

「UI State は UI が持つ」

という考え方を広めました。

 

🤔 Navigation 3 はさらに一歩進めた

Navigation 3 は、この流れをさらに推し進めています。

まず、画面引数を SavedStateHandle 経由で受け渡す必要がなくなりました。

各画面は自分専用の NavKey を持ちます。


data class UserDetail(
    val userId: Long
) : NavKey

さらに Navigation 3 では rememberSerializable が追加されました。

rememberSaveable が Android の保存・復元機構を利用するのに対し、

rememberSerializableNavEntry 単位 で UI State を保持します。

つまり、UI State は Navigation の Back Stack と一緒に移動するようになります。

画面ごとの State を ViewModel に保存する必要はなく、

各画面自身が UI State を持つ

という構造になります。

役割を整理すると次のようになります。


UI State
    │
rememberSerializable
    │
Composable


Navigation State
    │
NavKey


Presentation Logic
    │
ViewModel

UI State は UI のもの。

Navigation State は Navigation のもの。

どちらも、もう ViewModel が管理すべき State ではありません。

 

🤔 では ViewModel には何が残るのか?

UI State も Navigation State も ViewModel の役割ではなくなったとしたら、

ViewModel は何を担当するのでしょうか?

実は、やることはまだたくさんあります。


ViewModel
├── Repository の調整
├── ビジネスロジック
├── Flow の変換
├── Paging
├── Retry
└── viewModelScope

ここで面白いことがあります。

この中には、

State そのものはほとんどありません。

あるのは責務です。

ViewModel は

- Repository からデータを取得し
- ユーザー操作に応答し
- ビジネスロジックを実行し
- UI が描画しやすい形へ加工する

という役割を担います。

 

🤔 ViewModel は「State Holder」ではなく「State Producer」へ

例えば次のような ViewModel を考えてみます。


val uiState = combine(
    userRepository.users,
    settingsRepository.settings
) { users, settings ->
    HomeUiState(
        users = users,
        darkMode = settings.darkMode
    )
}.stateIn(...)

ここで実際の State はどこにあるでしょうか。

ViewModel ではありません。

データの正しい管理場所(Single Source of Truth)は Repository にあります。

ViewModel は、それらを UI 用に変換しているだけです。

つまり ViewModel は

State を保持する存在ではなく、State を生成する存在

になりつつあります。

一見小さな違いに思えますが、

Presentation Layer の考え方を大きく変える変化です。

 

🤔 State を持つ責任は、それぞれのレイヤーへ

Compose より前は、


ViewModel
├── UI State
├── Navigation State
└── Presentation Logic

という構成が一般的でした。

Navigation 3 では、


rememberSerializable
└── UI State

NavKey
└── Navigation State

ViewModel
└── Presentation Logic

へと役割が整理されます。

Compose は UI State を UI に戻しました。

Navigation 3 は Navigation State を NavKey に任せ、

UI State を NavEntry ごとに保持できるようにしました。

ViewModel が不要になったわけではありません。

責務がより明確になった のです。

 

🤔 よりシンプルなアーキテクチャ

その結果、アーキテクチャは次のように整理できます。


+----------------------+
|      Composable      |
|----------------------|
| UI State             |
| rememberSerializable |
+----------+-----------+
           |
           v
+----------------------+
|      ViewModel       |
|----------------------|
| Presentation Logic   |
| Flow Transformation  |
| Repository Calls     |
+----------+-----------+
           |
           v
+----------------------+
|      Repository      |
|----------------------|
| Persistent Data      |
| DataStore / Database |
+----------------------+

各レイヤーが、それぞれ最も適した責務を持ちます。

- Composable は UI State
- NavKey は Navigation State
- Repository は永続データ
- ViewModel は Presentation Logic

責務が自然に分離された構造です。

 

🧑🏻‍💻 まとめ

Navigation 3 が登場しても、ViewModel が不要になるわけではありません。

ViewModel はこれまでどおり、

- ビジネスロジック
- Repository の調整
- Flow の変換
- Paging
- 長時間動作する Coroutine

などを担当する最適な場所です。

一方で、Compose は rememberSaveable を導入し、UI State を UI に持たせやすくしました。

さらに Navigation 3 は rememberSerializable を追加し、UI State を NavEntry ごとに保持できるようにしました。また、Navigation State は NavKey が担当します。

その結果、「とりあえず ViewModel に State を置く」という設計は、必ずしも最適ではなくなりました。

これからは、

この State を ViewModel に保存するにはどうすればいいか?

ではなく、

そもそも、この State は ViewModel が持つべきものなのか?

と考える方が自然でしょう。

ViewModel は今でも欠かせない存在です。

しかし Navigation 3 の時代における主な役割は、State Holder ではなく Presentation Logic を担うことへと変化しつつあります。

なお、UI State を ViewModel の外へ移したからといって、Configuration Changes や Process Death に弱くなるわけではありません。

- UI State は rememberSaveablerememberSerializable
- Navigation State は NavKey
- 永続データは Repository

というように、それぞれのレイヤーが適切な仕組みを使えば、Configuration Changes と Process Death の両方に対応した堅牢なアプリケーションを構築できます。

重要なのは State を保存することではなく、

「その State を最も自然に所有すべきレイヤーが管理すること」なのです。

 

🧑🏻‍💻 参考


Related Categories :  AndroidDevelopmemtJetpackComposeKMPKotlinKotlin Multiplatform MobileNewbieRecommended