※ChatGPTを使用して記事を作成しています。
はじめに
Android開発において、ViewModelはもはや欠かせない存在です。
UIの状態管理、データ保持、ライフサイクルとの連携。
ActivityやFragmentの境界を超えてデータを安全に扱える、まさに救世主のような存在──。
…のはずでした。
しかし、そんな「ViewModel」を安易に共有した結果、私はLiveDataの暴走地獄に巻き込まれることになりました。
今回は、Fragment間でViewModelを共有したときに起きたリアルなバグとその修正方法を、実際のコードとともに紹介します。
きっかけ:Fragment間でデータを共有したい!
当時、開発していたアプリには次のような構成がありました。
- メイン画面(
MainFragment) - 詳細画面(
DetailFragment)
MainFragment のリストでアイテムを選ぶと、DetailFragment に選択したデータを表示する──という単純な流れです。
Fragment同士で選択データを共有する方法として、私は次のように考えました。
「ViewModelをActivityスコープで共有すれば、Fragment間通信いらないじゃん!」
これは多くの開発者が一度は通る道でしょう。
しかし、この判断が地獄の入り口でした。
失敗コード例:共有ViewModelで大混乱
class SharedViewModel : ViewModel() {
val selectedItem = MutableLiveData<Item>()
fun select(item: Item) {
selectedItem.value = item
}
}
MainFragment ではアイテム選択時に select() を呼び出します。
class MainFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter.onItemClick = { item ->
viewModel.select(item)
findNavController().navigate(R.id.action_main_to_detail)
}
}
}
DetailFragment では、その値を監視してUIに反映します。
class DetailFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.selectedItem.observe(viewLifecycleOwner) { item ->
binding.textView.text = item.name
}
}
}
この時点では「うまく動いてる!」と思いました。
しかし、アプリを何度か操作しているうちに、
謎の挙動 が発生しました。
発生したバグ
- 以前に選択したアイテムが別の画面で突然表示される
DetailFragmentを閉じてもLiveDataのイベントが再度発火する- 別のFragmentが意図せず
observe()の通知を受ける
まさに、LiveDataの幽霊現象。
UIが勝手に更新されたり、非表示Fragmentがデータを受け取ったり。
原因を探るため、Log.d を埋め込んで調査してみると──
D/ViewModel: selectedItem changed: Item(id=5)
D/DetailFragment: observed item: 5
D/DetailFragment: observed item: 5 ← なぜか2回目が来る!
再表示時に古い値が通知されていたのです。
原因:LiveDataは「最後の値」を覚えている
LiveDataは、最後にセットされた値を保持し続けるという特性を持ちます。
そのため、新しく observe() したタイミングで、過去の値が再通知されるのです。
さらに今回のようにActivityスコープでViewModelを共有している場合、Fragmentを閉じてもViewModelは生き続けるため、古いデータが新しいFragmentにも届いてしまうことになります。
修正版1:イベントを一度きりにする(SingleLiveEvent)
まず最初に試したのが、「一度だけ通知されるLiveData」に変更する方法です。
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val observers = HashMap<Observer<in T>, Observer<in T>>()
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
val wrapper = Observer<T> { t ->
if (t != null) {
observer.onChanged(t)
value = null // 一度だけ通知してリセット
}
}
observers[observer] = wrapper
super.observe(owner, wrapper)
}
override fun removeObserver(observer: Observer<in T>) {
val realObserver = observers.remove(observer)
super.removeObserver(realObserver ?: observer)
}
}
ViewModel側ではこれを使うようにします。
class SharedViewModel : ViewModel() {
val selectedItem = SingleLiveEvent<Item>()
fun select(item: Item) {
selectedItem.value = item
}
}
これにより、DetailFragment が再度作成された場合でも、古いイベントは再通知されません。
ただし、この方法にも欠点があります。
値を保持しないため、画面再生成(例: 画面回転)時にはデータが失われるのです。
修正版2:StateFlowで状態管理する
よりモダンで安全な方法がこちら。
StateFlow を使うと、値の再送や重複通知を制御しやすくなります。
class SharedViewModel : ViewModel() {
private val _selectedItem = MutableStateFlow<Item?>(null)
val selectedItem: StateFlow<Item?> = _selectedItem
fun select(item: Item) {
_selectedItem.value = item
}
fun clearSelection() {
_selectedItem.value = null
}
}
DetailFragment 側では、collect() で監視します。
lifecycleScope.launchWhenStarted {
viewModel.selectedItem.collect { item ->
item?.let {
binding.textView.text = it.name
}
}
}
StateFlowは常に最新値を保持しつつ、clearSelection() を呼ぶことで不要な再通知を防止できます。
さらに、Lifecycle と連動しているため、
非表示Fragmentが勝手に更新されることもありません。
修正版3:FragmentスコープのViewModelを使う
「そもそも共有しない」という選択肢も有効です。
Fragmentごとに独立したViewModelを使い、必要なデータだけを引き渡す設計です。
val viewModel: DetailViewModel by viewModels {
DetailViewModelFactory(itemId)
}
あるいは、SafeArgsなどでデータを安全に渡す方法もあります。
findNavController().navigate(
R.id.action_main_to_detail,
bundleOf("itemId" to item.id)
)
共有ViewModelは便利ですが、依存関係が強すぎると管理不能になります。
「スコープを広げる」ほど、バグの影響範囲も広がるのです。
教訓
この事件を通して学んだのは、「ViewModel共有」は魔法ではない、ということです。
Fragment間でデータを共有できるのは確かに便利ですが、適切なライフサイクル設計とスコープ管理をしなければ、思わぬ再通知・データ汚染・UI暴走 が簡単に起こります。
再発防止チェックリスト ✅
activityViewModels()は「状態共有が本当に必要なときだけ」使う- 一度きりのイベントには SingleLiveEvent / SharedFlow を使う
StateFlowで状態を明示的に管理する- Fragmentを再生成しても動作が破綻しないように設計する
- LiveDataの「最後の値が再通知される」特性を理解して使う
まとめ
ViewModelの共有は、設計次第で強力にも危険にもなります。
「どのスコープで状態を保持すべきか?」
「どのイベントは一度きりなのか?」
その線引きを明確にしないと、LiveDataが暴走してアプリが壊れます。

