― Snackbar / Navigation / Toast が UI を壊す理由 ―
※ChatGPTを使用して記事を作成しています。
はじめに
Jetpack Compose を使っていると、ある時ふとこんな設計になります。
data class UiState(
val showSnackbar: Boolean = false,
val navigateToDetail: Boolean = false
)
そして Composable 側でこう書きます。
if (state.showSnackbar) {
Snackbar(...)
}
一見、状態駆動UIとして正しそうに見えます。
しかしこれは、Composeにおける代表的な地雷設計です。
この記事では、
- なぜイベントを State にしてはいけないのか
- 何が起きるのか
- どう設計すべきか
を 失敗例 → 修正版 → 設計原則 の順で整理します。
1. State と Event は「似ているが別物」
まず最初に、定義をはっきりさせます。
✅ State とは
- 今この瞬間のUIの姿
- 再コンポーズされても保持されてよい
- 同じ値なら同じUIになる
例:
- ローディング中か
- エラーか
- 表示するリスト
❌ Event とは
- 一度だけ起きてほしい出来事
- 再コンポーズで再実行されてはいけない
- 消費されるもの
例:
- Snackbar を出す
- Toast を出す
- 画面遷移する
👉 State は「存在」
Event は「出来事」
ここを混同した瞬間、UI は壊れ始めます。
2. 失敗例①:Snackbar を State で制御する
❌ よくある実装
data class UiState(
val showSnackbar: Boolean = false
)
if (state.showSnackbar) {
Snackbar { Text("保存しました") }
}
ViewModel 側:
fun onSave() {
_state.update { it.copy(showSnackbar = true) }
}
❌ 何が問題か?
- 再コンポーズで 何度も Snackbar が出る
- 画面回転で 再表示
- Navigation 戻りで 再表示
- 「一度きり」が保証されない
つまり、
Snackbar が State に縛られてしまう
3. 失敗例②:Navigation を State にする
data class UiState(
val navigateDetail: Boolean = false
)
if (state.navigateDetail) {
navController.navigate("detail")
}
起きること
- 戻ると再遷移
- 二重遷移
- BackStack 崩壊
- 再現しない不具合
👉 「NavigationでBackStackが消えた日」案件
4. なぜ State にすると壊れるのか?
Compose の本質はこれです。
State は「再評価される前提」で作られている
- 再コンポーズはいつ起きてもおかしくない
- 同じ State なら同じ処理が再実行される
しかし Event は、
「一度きり」であることが前提
この前提が根本的に噛み合いません。
5. 正解①:LaunchedEffect + Event
Snackbar の正解例
ViewModel:
private val _event = MutableSharedFlow<UiEvent>()
val event = _event.asSharedFlow()
sealed class UiEvent {
data class ShowSnackbar(val message: String) : UiEvent()
}
fun onSave() {
viewModelScope.launch {
_event.emit(UiEvent.ShowSnackbar("保存しました"))
}
}
Composable:
LaunchedEffect(Unit) {
viewModel.event.collect { event ->
when (event) {
is UiEvent.ShowSnackbar ->
snackbarHostState.showSnackbar(event.message)
}
}
}
✔ 一度きり
✔ 再コンポーズ安全
✔ 状態と分離
6. 正解②:Navigation も Event にする
sealed class UiEvent {
object NavigateDetail : UiEvent()
}
LaunchedEffect(Unit) {
viewModel.event.collect { event ->
when (event) {
UiEvent.NavigateDetail ->
navController.navigate("detail")
}
}
}
Navigation は絶対に State にしない。
7. よくある誤解:「SingleLiveEvent は?」
結論から言います。
Compose では SingleLiveEvent を使う理由はありません
理由:
- Flow / SharedFlow が標準
- ライフサイクル安全
- 再購読問題がない
SingleLiveEvent は View時代の苦肉の策です。
8. State と Event の正しい責務分離
ViewModel が持つもの
- UI State(画面の状態)
- UI Event(一度きりの通知)
Composable がやること
- State を描画する
- Event を「消費」する
9. 設計チェックリスト(実務用)
次の質問に YES なら Event にすべきです。
- 一度だけ起きてほしい?
- 再表示されたら困る?
- 履歴として残らなくていい?
- UIの見た目そのものではない?
まとめ
Compose で最も多い設計ミスは、
イベントを State にしてしまうこと
です。
- State は「存在」
- Event は「出来事」
この線を越えない限り、
- 無限再実行
- 再現しないバグ
- BackStack 崩壊
は ほぼ防げます。

