※ChatGPTを使用して記事を作成しています。
「画面間で状態を共有したいだけだった」――それが、すべての始まりだった。
Jetpack ViewModelの登場で、ライフサイクルを意識せずにデータを扱えるようになった…はずだったのに、気づけばUIが暴走し、リストが意図せず更新され、果てにはメモリリークでアプリが落ちる。
今回は、ViewModelのスコープを誤って共有してしまったことで地獄を見た話をお届けする。
1. 状態共有の甘い誘惑
当時の私は「ViewModelを共有すれば、Fragment間で簡単にデータをやり取りできる」と信じていた。
例えば、リスト画面と詳細画面。リストで選んだアイテムを詳細画面で表示したい。
それならこう書けばいいじゃないか、と。
class SharedViewModel : ViewModel() {
val selectedItem = MutableLiveData<Item>()
}
そして両方のFragmentでこうやって取得した。
private val viewModel: SharedViewModel by activityViewModels()
テスト時は完璧だった。
リストを選択 → 詳細画面に遷移 → データ表示、バッチリ。
「これがViewModelの正しい使い方だ!」と胸を張っていた。
だが、この「activityViewModels()」がすべての地獄の始まりだった。
2. 状態が勝手に更新される怪奇現象
しばらくすると、別のFragmentでも同じSharedViewModelを使いたくなった。
たとえば、「お気に入り」機能を別タブで追加したときのこと。
そのとき、謎の現象が発生した。
お気に入りタブを開くと、なぜか前に開いていた詳細画面のデータが再表示される。selectedItemの値が勝手に復活するのだ。
調べてみると、activityViewModels()で取得しているため、同じActivity内の全Fragmentで同一インスタンスが共有されていた。
つまり、ライフサイクルが異なる画面間でデータが残り続けていた。
私の想定では「Fragmentが消えたらViewModelも破棄される」と思っていた。
しかし実際は、「Activityが破棄されない限り、ViewModelも生き続ける」だった。
3. onClearedが呼ばれない問題
さらに悪夢は続く。
詳細画面から戻っても、ViewModelのonCleared()が呼ばれない。
class SharedViewModel : ViewModel() {
override fun onCleared() {
Log.d("SharedViewModel", "Cleared")
}
}
いつまで経ってもこのログが出ない。
Fragmentを閉じても、ViewModelはActivityスコープだから破棄されない。
つまり、古いデータやリスナーが生き続け、どんどんリークしていく。
結果、画面を行き来するたびにLiveDataが複数回observeされ、UIが二重三重に更新される地獄が完成した。
4. 修正版:Fragmentごとにスコープを分ける
この問題を解消するため、まず「スコープ」を正しく理解する必要があった。
| スコープ | ViewModelの生存期間 | 主な用途 |
|---|---|---|
viewModels() | Fragmentごと | 画面単位の状態管理 |
activityViewModels() | Activity内で共有 | 複数Fragmentで共有 |
navGraphViewModels() | Navigation Graph単位 | 画面遷移間で共有(限定的) |
今回のケースでは、
「リスト → 詳細画面」という遷移ごとに状態を独立させたかった。
そこで次のように修正した。
private val viewModel: SharedViewModel by viewModels()
これにより、Fragmentを破棄するとViewModelも自動的に破棄され、onCleared()が呼ばれる。
不要なデータ保持もなくなり、リークも解消。
「FragmentごとにViewModelを持たせる」ことでようやく安定した。
5. 本当に共有すべき場面とは
では、「activityViewModels()」は使ってはいけないのか?
答えは「使っていいが、スコープを明確に意識すること」だ。
実際、ログイン状態やテーマ設定のように、アプリ全体で共有したい状態には最適だ。
ただし、一時的なデータ共有には不向き。
Navigation Graphスコープを利用したほうが安全だ。
private val viewModel: SharedViewModel by navGraphViewModels(R.id.nav_graph_main)
この方法なら、特定の画面グループだけで共有できる。
Activity全体よりもスコープが限定され、予期せぬデータ干渉を防げる。
6. 実践でやらかしやすいパターン
特に注意が必要なのは、以下のようなケースだ。
✅ 画面Aと画面Bが別のタブにある場合
→ Activityが共通でも、ViewModelを共有してはいけない。
バックスタック上で生存しているFragment同士が干渉する。
✅ 非同期処理でLiveDataをobserveしている場合
→ 複数のObserverが生き残り、二重更新になる。removeObservers(viewLifecycleOwner)を適切に呼ぶこと。
✅ SavedStateHandleを活用していない場合
→ 状態復元が難しく、破棄のタイミングが読めない。
7. 教訓:スコープを意識しないViewModel共有は爆弾
今回の失敗で痛感したのは、
「ViewModelは“どこまで生きるか”を決める必要がある」ということ。
スコープを誤ると、
- データの重複
- メモリリーク
- UIの異常更新
など、見えない地雷を踏む確率が一気に上がる。
特にNavigationを併用する現代のアプリ構成では、
スコープ設計がそのままアーキテクチャの安定性を決める。
8. まとめ
activityViewModels()は強力だが、扱いを誤ると地獄を見る- 画面単位で完結するデータは
viewModels()で管理すべし - 限定的に共有するなら
navGraphViewModels()が最適 onCleared()が呼ばれないViewModelには必ず理由がある

