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 の保存・復元機構を利用するのに対し、
rememberSerializable は NavEntry 単位 で 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 は rememberSaveable や rememberSerializable
- Navigation State は NavKey
- 永続データは Repository
というように、それぞれのレイヤーが適切な仕組みを使えば、Configuration Changes と Process Death の両方に対応した堅牢なアプリケーションを構築できます。
重要なのは State を保存することではなく、
「その State を最も自然に所有すべきレイヤーが管理すること」なのです。
🧑🏻💻 参考
Related Categories : Android・Developmemt・JetpackCompose・KMP・Kotlin・Kotlin Multiplatform Mobile・Newbie・Recommended