
Jetpack Compose で Snackbar、Navigation、Toast のような「一度だけ処理したいイベント」をどう扱うかは、多くの Android エンジニアが一度は悩むポイントです。
私も長い間 SharedFlow を使っていました。
しかし最終的には、Compose の UI イベント用途では Channel を使うようになりました。
この記事では、
- なぜ SharedFlow で問題が起きやすいのか
- なぜ Channel の方が自然だったのか
- Compose のライフサイクルと何が噛み合わないのか
を整理してみます。
🧑🏻💻 One-Time Event とは?
まず前提として、Compose には大きく 2 種類のデータがあります。
State
└─ 画面が「今どういう状態か」
Event
└─ 一度だけ実行したいもの
例えば:

State は「現在値」が重要です。
しかし Event は違います。
「発生した」という事実だけ重要
つまり:
- 1回だけ消費したい
- 再購読時に再実行したくない
- 画面回転で再発火したくない
という特徴があります。
🧑🏻💻 SharedFlow を使っていた頃
最初は多くのサンプルと同じように SharedFlow を使っていました。
private val _event = MutableSharedFlow<UiEvent>()
val event = _event.asSharedFlow()
UI 側では:
LaunchedEffect(Unit) {
viewModel.event.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(event.message)
}
}
}
}
一見すると問題なさそうです。
しかし実運用では、徐々に違和感が出てきました。
🧑🏻💻 Collector がいないとイベントが消える
SharedFlow(replay = 0) は、
購読者がいない時のイベントを保持しません
つまり:
ViewModel emits event
↓
Compose がまだ collect 開始していない
↓
イベント消失
が起きます。
特に Compose では:
- 画面遷移直後
- Lifecycle 切り替え
- BackStack 復帰
- 一時的な recomposition
などで collector の開始タイミングがズレやすいです。
🧑🏻💻 replay = 1 にすると今度は別問題
では replay = 1 にすれば良いのでしょうか?
MutableSharedFlow(
replay = 1
)
これは確かにイベント消失を防ぎます。
しかし次は:
画面回転したら
昔のイベントが再配信される問題が発生します。
例えば:
Snackbar 表示
↓
画面回転
↓
Snackbar 再表示
これは UX 的にかなり不自然です。
Navigation だとさらに危険です。
画面回転しただけで
再度 navigate される
ことがあります。
🧑🏻💻 SharedFlow は「共有ストリーム」
ここで改めて考えると、SharedFlow は本来:
複数 collector に
同じデータを共有する
ための仕組みです。
つまり設計思想としては:
- 状態通知
- Broadcast
- Hot stream
寄りです。
一方 One-Time Event は:
- 1回だけ消費したい
なので、実は思想が少しズレています。
🧑🏻💻 Channel に変えた
そこで最終的にこうなりました。
private val _event = Channel<UiEvent>(
capacity = Channel.BUFFERED
)
val event = _event.receiveAsFlow()
送信:
viewModelScope.launch {
_event.send(
UiEvent.ShowSnackbar("Saved")
)
}
受信:
LaunchedEffect(Unit) {
viewModel.event.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(event.message)
}
}
}
}
🧑🏻💻 なぜ Channel の方が自然だったのか
Channel は:
Queue
です。
つまり:
送る
↓
溜まる
↓
1個ずつ消費
になります。
これは One-Time Event の性質にかなり近いです。
特に:
- Snackbar
- Navigation
- Dialog
- Toast
のような
順番に1回だけ処理したい
イベントと相性が良いです。
🧑🏻💻 BUFFERED が重要
Compose では collector が少し遅れて開始することがあります。
そのため:
Channel.BUFFERED
を使うことで、
collector 開始前のイベント
もある程度保持できます。
これは SharedFlow(replay = 0) よりかなり扱いやすく感じました。
🧑🏻💻 ただし Channel にも注意点はある
もちろん Channel が万能ではありません。
特に重要なのは:
基本的に single consumer 前提
である点です。
つまり:
複数 collector が同時に collect
すると、どこが消費するか不定になります。
そのため:
UI Event 専用
として割り切るのが重要です。
🧑🏻💻 個人的な使い分け
現在は大体こう整理しています。

かなり安定しました。
🧑🏻💻 Compose では「State」と「Event」を分けるのが重要
Compose は State 管理が非常に強力です。
しかしその反面、
Event を State と同じ感覚で扱う
と事故りやすいです。
特に:
- Snackbar
- Navigation
- Dialog
- Permission request
などは:
状態ではなく副作用
として考えた方が整理しやすいです。
🧑🏻💻 まとめ
SharedFlow は優秀ですが、
One-Time Event に完全最適
とは限りません。
Compose のライフサイクルと組み合わさると:
- イベント消失
- replay 問題
- collector timing 問題
に遭遇しやすいです。
一方 Channel は:
FIFO queue
として動作するため、
- 一度だけ処理したい
- 順番保証したい
- 再配信したくない
という UI Event とかなり自然に噛み合いました。
最近は:
State は StateFlow
Event は Channel
という形に落ち着いています。





