※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()を使えば、HomeFragmentとEditProfileFragmentで同じインスタンスを共有できる。
しかも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で共有を明示的に行うのが安全

