1. SharedFlowとは?
SharedFlow は 複数の購読者に同時配信できるホットなフロー です。
StateFlow のように「最新値を保持」せず、
主に 一度だけ発火するイベント(One-shot Event) の通知に適しています。
一言で言うと:
SharedFlow= 「複数の購読者に同時に配るイベントストリーム」
UIイベント(例:Toast表示、Navigation遷移、Dialog表示)にぴったりです。
2. StateFlowとの違い
| 項目 | StateFlow | SharedFlow |
|---|---|---|
| 目的 | 状態管理 | イベント通知 |
| 最新値保持 | ✅ する | ❌ しない(replay=0) |
| 初期値 | 必須 | 不要 |
| 更新方法 |
.value / .update
|
.emit() / .tryEmit()
|
| 冪等性 | 状態更新のみに使う | 単発の発火用 |
| 再購読時の挙動 | 最新値を即通知 | 過去イベントは再送されない |
| Compose再構築時 | 値が保持される | 再通知されない(One-shot) |
3. 基本構文
// MutableSharedFlowを作成
private val _events = MutableSharedFlow<String>(
replay = 0, // 過去のイベントを再送しない
extraBufferCapacity = 1 // バッファ1件(tryEmit成功しやすく)
)
val events: SharedFlow<String> = _events
// 発火
_events.tryEmit("Hello")
// 購読
viewModel.events.collect { msg ->
println("イベントを受信: $msg")
}
4. One-shot イベント設計パターン(ViewModel)
SharedFlow の本領発揮は「UI単発イベント」にあります。
例:ログイン成功時にナビゲーション+Toast表示
sealed interface UiEvent {
data object NavigateHome : UiEvent
data class ShowToast(val message: String) : UiEvent
}
@HiltViewModel
class LoginViewModel @Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1
)
val events: SharedFlow<UiEvent> = _events
fun onLoginClicked(id: String, pass: String) = viewModelScope.launch {
runCatching { loginUseCase(id, pass) }
.onSuccess { _events.tryEmit(UiEvent.NavigateHome) }
.onFailure { _events.tryEmit(UiEvent.ShowToast("ログイン失敗")) }
}
}
5. Compose側での安全な購読方法
LaunchedEffect + repeatOnLifecycle を組み合わせて
再組成・画面回転でも多重購読を避ける のがポイント。
@Composable
fun LoginScreen(
vm: LoginViewModel = hiltViewModel(),
onNavigateHome: () -> Unit,
showToast: (String) -> Unit
) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
LaunchedEffect(Unit) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.events.collect { event ->
when (event) {
UiEvent.NavigateHome -> onNavigateHome()
is UiEvent.ShowToast -> showToast(event.message)
}
}
}
}
}
ポイント
-
LaunchedEffect(Unit)により Compose 再構築でも重複しない -
repeatOnLifecycleにより STOPPED 時に自動で購読解除 -
replay=0により再購読時に古いイベントは再発火しない
6. SharedFlow のパラメータ設計
replay
- 新しい購読者に「過去のイベント」を何件再送するか
- UIイベントでは 必ず
0(再発火防止)
extraBufferCapacity
- 一時バッファサイズ。非同期で
tryEmit()したいときに便利 - UIイベントなら 1〜2 が推奨
onBufferOverflow
- 溢れたときの挙動 (
SUSPEND,DROP_OLDEST,DROP_LATEST) - デフォルトは
SUSPEND - UIイベントなら
DROP_OLDESTが安全
val _event = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
7. SharedFlow vs Channel(どちらを使うべき?)
| 特徴 | SharedFlow | Channel |
|---|---|---|
| 複数購読者 | 複数に同時配布 | 単一のみ |
| イベント確実性 | バッファ次第でロス可 | 受信されるまで保持 |
| Flowとの統合 | そのまま collect
|
要 receiveAsFlow()
|
| Backpressure制御 | 柔軟 (BufferOverflow) |
明確 (send/receive) |
| UIイベント | 推奨 | 小規模なら可 |
| テスト容易性 | 高い | 高い(ただし単一受信) |
目安
- 「複数画面や複数購読者にイベントを配信したい」→
SharedFlow - 「UI一箇所で確実に1回だけ受けたい」→
Channel + receiveAsFlow()
8. Clean Architecture での使い分け
| 層 | 推奨設計 | 説明 |
|---|---|---|
| UseCase層 | Flow<Result> |
業務結果をFlowで返す |
| ViewModel層 |
StateFlow(状態) + SharedFlow(イベント) |
状態とイベントを分離 |
| UI層 |
collectAsState() + collect()
|
状態とイベントを購読 |
9. よくある落とし穴と対策
| 失敗例 | 問題点 | 対策 |
|---|---|---|
StateFlowでイベントを送る |
再購読で再実行(ナビゲーション多重発火) |
SharedFlowを使う |
replay > 0 |
再購読で過去イベント再発火 |
replay=0に設定 |
collectをUIの@Composable内で直接呼ぶ |
再組成で多重購読 |
LaunchedEffect + repeatOnLifecycle
|
emit()がサスペンドして遅延 |
バッファ不足 |
extraBufferCapacity=1 + tryEmit()
|
| 同期的にイベントをテストできない | 非同期競合 |
runTest + advanceUntilIdle()
|
10. SharedFlow のテスト例
@OptIn(ExperimentalCoroutinesApi::class)
class LoginViewModelTest {
@get:Rule
val main = MainDispatcherRule()
@Test
fun emits_navigateHome_event() = runTest {
val vm = LoginViewModel(loginUseCase = { }) // 成功と仮定
val results = mutableListOf<UiEvent>()
val job = launch { vm.events.take(1).toList(results) }
vm.onLoginClicked("a", "b")
advanceUntilIdle()
assertTrue(results.first() is UiEvent.NavigateHome)
job.cancel()
}
}
11. 応用:状態 × イベント のハイブリッド設計
多くのアプリでは StateFlow と SharedFlow を組み合わせて使います。
data class UiState(
val loading: Boolean = false,
val user: User? = null
)
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events
fun fetch() = viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
runCatching { repo.loadUser() }
.onSuccess { user ->
_uiState.value = UiState(loading = false, user = user)
_events.tryEmit(UiEvent.ShowToast("読み込み成功"))
}
.onFailure {
_uiState.update { it.copy(loading = false) }
_events.tryEmit(UiEvent.ShowToast("失敗しました"))
}
}
}
まとめ
| 用途 | クラス | 特徴 |
|---|---|---|
| UI状態 | StateFlow |
常に最新値を保持し、UI再生成でも再表示 |
| One-shotイベント | SharedFlow(replay=0) |
消費型の単発イベント |
| 確実な1回配信 | Channel + receiveAsFlow() |
受信完了まで保持 |
| 複数購読者に配信 | SharedFlow |
同報・ブロードキャスト |
「状態は
StateFlow、イベントはSharedFlowに分離」
これが Kotlin × Compose × MVVM の黄金設計。
SharedFlow は「一度きりの出来事」を安全・明確に扱うための標準的な手法です。
正しく使えば、ナビゲーションの多重実行・Toastの再発火 といった
UIの“バグっぽい挙動”をきれいに防止できます。