ViewModelを共有したら地獄を見た話 〜スコープを誤解した代償〜 

※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には必ず理由がある

タイトルとURLをコピーしました