ViewModelを共有したら地獄を見た話 ―― スコープの境界を見誤ると、すべてが巻き込まれる。

※ChatGPTを使用して記事を作成しています。

■ 序章:突然、別画面のデータが変わった日

ある朝、テスト端末を眺めていた後輩の顔が青ざめた。
「先輩、これ……プロフィール画面で名前を変更したら、
 ホーム画面の表示まで勝手に変わるんですけど……」

まさか、と思って私も操作してみた。
確かに、プロフィール編集画面でユーザー名を変更すると、ホーム画面に表示されている名前までリアルタイムで更新されていた。

しかも、別画面のViewModelまで巻き込んでいる

そのときは「LiveDataって便利だな」と笑っていたが、後にそれが地獄の入り口だったことを知る。

■ 第一章:共有ViewModelという甘い誘惑

当時の私は、「画面間でデータを共有したい」ときに最初に思いついたのが「ViewModelを共有する」だった。

公式ドキュメントにも、こうある。

複数のFragmentで同じViewModelを共有する場合、
activityViewModels()を使用します。

「なるほど、Activityスコープにすればデータ共有できるのか!」
──これがすべての始まりだった。

■ 第二章:そして、すべてがつながった

当時のコードはこんな感じだった。

class ProfileViewModel : ViewModel() {
    val userName = MutableLiveData<String>()
}

class HomeFragment : Fragment() {
    private val viewModel: ProfileViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewModel.userName.observe(viewLifecycleOwner) { name ->
            binding.textUserName.text = name
        }
    }
}

class EditProfileFragment : Fragment() {
    private val viewModel: ProfileViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.buttonSave.setOnClickListener {
            viewModel.userName.value = binding.editUserName.text.toString()
        }
    }
}

activityViewModels()を使えば、HomeFragmentEditProfileFragmentで同じインスタンスを共有できる。
しかもLiveDataで自動反映。
──完璧に思えた。

だが、アプリが複雑になるにつれ、事態は一変する。

■ 第三章:スコープ地獄のはじまり

後日、他の開発者が「設定画面でも同じユーザー情報を使いたい」と言い出した。
「Activityスコープで共有してるから大丈夫だよ」と答えた私。
が、リリース後にユーザーから報告が相次いだ。

「設定画面を開いたら、入力中の内容が消えた」
「戻るボタンを押したら、ホーム画面の名前が変わってた」

調べてみると、Fragment間で意図せずデータが上書きされていた。

原因は単純。
ActivityスコープのViewModelを共有しているため、どのFragmentからも同じインスタンスを参照している。

そのため、あるFragmentで値を更新すると、他のFragmentでも自動的に変更が伝搬していた。

つまり、「共有ViewModel」は便利どころか、アプリ全体の状態を巻き込む爆弾になっていたのだ。

■ 第四章:さらに深まる混乱

問題はデータの共有だけではない。
ライフサイクルのタイミングでも地獄が訪れた。

ActivityスコープのViewModelは、Activityが破棄されるまで生き続ける。
つまり、Fragmentをいくら切り替えても、ViewModelはリセットされない

一方、FragmentのUIは再生成されるため、LiveData.observe()が再度呼ばれる。

結果──

  • 二重でobserveされて通知が2回発火
  • 古いデータがUIに一瞬だけ反映
  • メモリリークが発生

と、まさに地獄の三重奏

■ 第五章:失敗コードの実例

class SharedViewModel : ViewModel() {
    val message = MutableLiveData<String>()
}

class FragmentA : Fragment() {
    private val viewModel: SharedViewModel by activityViewModels()

    override fun onResume() {
        super.onResume()
        viewModel.message.observe(viewLifecycleOwner) { msg ->
            println("FragmentA observes: $msg")
        }
    }
}

class FragmentB : Fragment() {
    private val viewModel: SharedViewModel by activityViewModels()

    fun updateMessage() {
        viewModel.message.value = "Updated!"
    }
}

このコードでは、FragmentAを再表示するたびに observe()が追加され、updateMessage()を呼ぶとログが2回、3回、4回…と増えていく。

「なぜこんなことに?」
それはスコープを理解していなかったからだ。

■ 第六章:修正版 ― スコープを適切に切る

根本的な解決策は、「スコープを明確に分ける」こと。
共有したいのが一部のデータだけなら、ViewModelを分離するべきだ。

修正版はこう。

class HomeViewModel : ViewModel() {
    val userName = MutableLiveData<String>()
}

class EditProfileViewModel : ViewModel() {
    val tempName = MutableLiveData<String>()
}

そしてFragment側で、必要な範囲だけViewModelを保持する。

// Home画面(Activityスコープではない)
private val viewModel: HomeViewModel by viewModels()

// 編集画面(個別スコープ)
private val editViewModel: EditProfileViewModel by viewModels()

どうしても共通データが必要な場合は、リポジトリ層で状態を管理するか、NavigationのSavedStateHandleを使って受け渡すのが安全だ。

navController.currentBackStackEntry
    ?.savedStateHandle
    ?.set("edited_name", "新しい名前")

戻る際に取得:

navController.previousBackStackEntry
    ?.savedStateHandle
    ?.getLiveData<String>("edited_name")
    ?.observe(viewLifecycleOwner) { name ->
        viewModel.userName.value = name
    }

こうすることで、データの共有スコープを明示的に制御できる。

■ 第七章:スコープを誤解すると地獄を見る

ViewModelには主に3つのスコープがある。

スコープ寿命主な使いどころ
viewModels()Fragment単位単一画面で完結するデータ
activityViewModels()Activity単位複数Fragmentで共有したい状態
navGraphViewModels()Navigationグラフ単位特定の画面群で共有

このうち、activityViewModels()は最も強力だが、誤用するとすべてを巻き込む

実際、今回のように「Activity内の全Fragmentでデータが共有されてしまう」ため、意図しないUI更新や状態の衝突が頻発する。

■ 第八章:地獄からの帰還 ― 教訓

リファクタリング後、各Fragmentが独立したViewModelを持つようにした。
結果、データが勝手に共有されることはなくなり、クラッシュも大幅に減少した。

そして何より、UIの挙動が安定した。

私はようやく気づいた。
ViewModelのスコープとは、単なる寿命の問題ではなく、責務の境界線でもあるのだと。

■ 終章:今日の教訓

「共有ViewModelは、共有責任でもある。」

便利だからといってスコープを広げすぎると、その責任範囲も際限なく広がる。

“再利用”と“乱用”の境界は紙一重だ。
そして、その一線を越えた瞬間、あなたのアプリは地獄を見る

■ まとめ

  • activityViewModels()は強力だが、誤用すると状態衝突を招く
  • スコープは「寿命」ではなく「責務の境界」として設計する
  • SavedStateHandleやRepositoryで共有を明示的に行うのが安全
タイトルとURLをコピーしました