SharedFlow はKotlin Coroutinesライブラリのホットリアクティブフローであり、画面回転やサブスクライバの再作成時に繰り返されるべきではない単発イベント(one-shot events)向けに最適化されています。SharedFlowがStateFlowとどのように異なるかを示します。StateFlowとは異なり、SharedFlowは新しいサブスクライバのために最後の値を保存せず、replay、extraBufferCapacity、onBufferOverflowの設定をサポートします。Google(Android Developers、2025)によると、SharedFlowはナビゲーションコマンド、Snackbarメッセージ、および正確に1回処理されるべきその他のイベントに推奨されるソリューションです。
重要ポイント
SharedFlow はkotlinx.coroutines.flowライブラリのホットフローであり、StateFlowとは異なり、単一の状態に結びつかず、任意のサブスクライバに任意の数のイベントを発行できます。SharedFlowはStateFlowのベース型です。StateFlowは実際にはreplay = 1のSharedFlowを介して実装されています。
SharedFlowの主な特徴は、最後の値を保存する必要がないことです。デフォルト(replay = 0)では、新しいイベントが送信されるまで新しいサブスクライバは何も受け取りません。これにより、SharedFlowはイベントを正確に1回処理する必要があるシナリオ(ナビゲーション、Snackbar、システム通知、QRコードスキャン結果)に最適です。
SharedFlowはkotlinx.coroutines 1.4.0(2020年11月)でStateFlowとともに安定化されました。Kotlin Coroutinesのドキュメント(2025年)によると、SharedFlowはサブスクライバの同期にきめ細かいロックを使用し、JetBrainsのテストで確認されたパフォーマンス低下なしに、1000以上の同時サブスクライバまで線形スケーラビリティを提供します。
SharedFlowとStateFlowの選択は、転送されるデータのセマンティクス(状態はStateFlow、イベントはSharedFlow)に依存します。以下に、例を示しながら明確な基準を示します。
| 基準 | SharedFlow | StateFlow |
|---|---|---|
| セマンティクス | 単発イベント(ナビゲーション、トースト、アラート) | UI状態(リスト、ローディング、エラー) |
| 初期値 | 不要 | 必須 |
| サブスクリプション時の再生 | replay > 0の場合のみ | 常に最後の値 |
| 結合 | なし — イベントは失われません(バッファが満杯でない場合) | あり — 最新のみ保存 |
| バッファリング | replay + extraBufferCapacityで設定可能 | 1のみ(replay=1固定) |
| 使用法 | navigationEvent、showSnackbar、openDialog | items、isLoading、uiState |
最も単純なルール:画面回転時にデータを表示する必要がある場合は状態(StateFlow)。画面回転時にイベントを繰り返してはならない場合は単発イベント(SharedFlow)。たとえば、エラーメッセージのトーストはSharedFlow:回転時にトーストを再度表示してはいけません。商品リストはStateFlow:回転時にリストは画面に残る必要があります。
IT Sectrでは、SharedFlowを次の用途で使用しています:ナビゲーションコマンド(画面遷移、ディープリンク開封)、UIイベント(Snackbar、AlertDialog)、システム通知(バックグラウンドデータ更新、支払い結果)、分析イベント(ロギング、トラッキング)。
MutableSharedFlow はSharedFlowの可変バージョンで、イベント送信用にemit()(suspend)およびtryEmit()(非suspend)メソッドを提供します。emit()は、バッファが満杯でonBufferOverflow = SUSPENDの場合に一時停止します。tryEmit()は、イベントがバッファに正常に追加されたかどうかを示すBooleanを返します。
class EventBus {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> get() = _events
suspend fun sendEvent(event: UiEvent) {
_events.emit(event)
}
fun trySendEvent(event: UiEvent): Boolean {
return _events.tryEmit(event)
}
}
sealed interface UiEvent {
data class ShowSnackbar(val message: String) : UiEvent
data class NavigateTo(val route: String) : UiEvent
data class ShowDialog(val title: String, val message: String) : UiEvent
}
コンストラクタパラメータは非常に重要です。replay = 0は、イベントが新しいサブスクライバに対して繰り返されないことを保証します。extraBufferCapacity = 10は、UIがサブスクライブする前に高速なイベント発行のためのバッファを提供します。DROP_OLDESTはオーバーフロー戦略で、古いイベントは破棄され、新しいイベントが保持されます。Kotlin Coroutines Performance(JetBrains、2024年)によると、extraBufferCapacity = 64のSharedFlowは、損失なく毎秒100,000以上のイベントを処理します。
Eventパターン(またはUiEvent) は、ViewModelからViewに単発イベントを渡すためのGoogle推奨の方法です。状態(StateFlow)とは異なり、イベントは正確に1回処理されるべきであり、画面回転時に繰り返されてはいけません。replay = 0のSharedFlowはこのタスクに最適です。
class CheckoutViewModel : ViewModel() {
private val _uiState = MutableStateFlow<CheckoutState>(CheckoutState.Idle)
val uiState: StateFlow<CheckoutState> get() = _uiState
private val _event = MutableSharedFlow<CheckoutEvent>()
val event: SharedFlow<CheckoutEvent> get() = _event
fun placeOrder() {
viewModelScope.launch {
_uiState.value = CheckoutState.Loading
try {
val orderId = orderRepository.createOrder(cart)
_uiState.value = CheckoutState.Success(orderId)
_event.emit(CheckoutEvent.NavigateToOrderTracking(orderId))
} catch (e: Exception) {
_uiState.value = CheckoutState.Error(e.message)
_event.emit(CheckoutEvent.ShowErrorSnackbar(e.message ?: "レイアウトエラー"))
}
}
}
}
sealed interface CheckoutEvent {
data class NavigateToOrderTracking(val orderId: String) : CheckoutEvent
data class ShowErrorSnackbar(val message: String) : CheckoutEvent
}
View(Activity/Fragment)側:イベントのサブスクリプションは、lifecycleScopeでrepeatOnLifecycle(STATE.STARTED)を使用して行う必要があります。STARTEDに入るたびにサブスクリプションは再作成されますが、replay=0のSharedFlowはすでにイベントを解放しているため、イベントは繰り返されません。これにより、注文追跡画面へのナビゲーションが1回のみ発生し、回転のたびに発生しないことが保証されます。
MutableSharedFlowのコンストラクタは、バッファの動作を決定する3つのパラメータを受け入れます。誤った設定は、イベントの損失やemit()のブロックにつながる可能性があります。
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
| replay | Int | 0 | 新しいサブスクライバに再生される最近のイベント数。0 = 再生しない、1 = StateFlowのように |
| extraBufferCapacity | Int | 0 | replayを超える追加バッファ。イベントは循環バッファに保存されます。ほとんどのシナリオで推奨される上限は64 |
| onBufferOverflow | BufferOverflow | SUSPEND | バッファ満杯時の戦略:SUSPEND、DROP_OLDEST、DROP_LATEST |
// さまざまなシナリオの設定:
// 1. 単発UIイベント(ナビゲーション、トースト)
val uiEvents = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 5,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
// 2. 状態同期のためのリプレイフロー(StateFlowのように)
val stateLike = MutableSharedFlow<AppState>(
replay = 1,
extraBufferCapacity = 0
)
// 3. 高頻度イベント発行(分析、ログ)
val analytics = MutableSharedFlow<AnalyticsEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
重要: extraBufferCapacity + replay = 合計バッファサイズです。サブスクライバがイベントを処理するよりも速くemit()が呼び出されると、バッファが満杯になり、onBufferOverflowが作動します。UIイベントの場合、DROP_OLDESTは安全な戦略です。古いイベント(もはや関連性のないナビゲーション)は新しいイベントのために破棄されます。金融取引の場合は、SUSPENDを使用してください。これにより、送信者をブロックする代償として、イベントが失われないことが保証されます。
ナビゲーションコマンドは、SharedFlowの古典的なユースケースです。Fragmentがイベントをサブスクライブし、ナビゲーションを実行します。画面回転時にコマンドは繰り返されません。
// ViewModel
class AuthViewModel : ViewModel() {
private val _navEvent = MutableSharedFlow<NavEvent>()
val navEvent: SharedFlow<NavEvent> get() = _navEvent
fun onLoginSuccess() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.HOME))
}
}
fun onLogout() {
viewModelScope.launch {
_navEvent.emit(NavEvent.NavigateTo(NavRoutes.LOGIN))
}
}
}
sealed interface NavEvent {
data class NavigateTo(val route: String) : NavEvent
data class NavigateBack(val popUpTo: String? = null) : NavEvent
}
// Fragment内:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.navEvent.collect { navEvent ->
when (navEvent) {
is NavEvent.NavigateTo -> findNavController().navigate(navEvent.route)
is NavEvent.NavigateBack -> findNavController().popBackStack()
}
}
}
}
複雑なシナリオ:UI用のStateFlowと組み合わせたバックグラウンドイベント通知用のSharedFlow。
class NotificationViewModel : ViewModel() {
private val _toastMessage = MutableSharedFlow<String>()
val toastMessage: SharedFlow<String> get() = _toastMessage
private val _notifications = MutableStateFlow<List<Notification>>(emptyList())
val notifications: StateFlow<List<Notification>> get() = _notifications
init {
viewModelScope.launch {
notificationChannel
.consumeAsFlow()
.collect { notification ->
_notifications.value = _notifications.value + notification
_toastMessage.emit("新しい通知: ${notification.title}")
}
}
}
fun dismissNotification(id: String) {
_notifications.value = _notifications.value.filter { it.id != id }
}
fun markAllRead() {
viewModelScope.launch {
_notifications.value = _notifications.value.map { it.copy(isRead = true) }
_toastMessage.emit("すべての通知を既読としてマーク")
}
}
}
この例では:StateFlowは通知リストを保存し(状態 — 回転時に保持)、SharedFlowはトーストメッセージを発行します(単発イベント — 回転時に繰り返されません)。2種類のFlowの組み合わせは、2022年以降のViewModelに対するGoogle推奨のパターンです。
よくある質問
はい、バッファが満杯でonBufferOverflow = DROP_OLDESTまたはDROP_LATESTの場合に発生します。SharedFlowはすべてのイベントの配信を保証しません。これはメッセージキュー(Channelなど)ではありません。すべてのイベントの確実な配信が必要な場合は、無制限バッファ(UNLIMITED)またはBroadcastChannel(非推奨)のChannelを使用してください。UIイベントの場合、古いイベント(古いナビゲーションなど)の損失は期待される動作であり、バグではありません。
Channel はFIFOキューであり、各イベントは正確に1つのサブスクライバに配信されます(ポイントツーポイント)。SharedFlowはブロードキャストであり、各イベントはすべてのアクティブなサブスクライバに配信されます。SharedFlowはBroadcastChannel(非推奨)に近く、1対多のシナリオに適しています。Channelは1対1(スレッドプール、パイプライン)用です。JetBrainsの推奨によると、SharedFlowはすべての新しいプロジェクトでBroadcastChannelの代替です。
SharedFlowはすでにスレッドセーフです。emit()とcollect()は適切に同期されています。複数のスレッドがロックなしでemit()を呼び出すことができ、すべてのアクティブなサブスクライバは正しい順序でイベントを受け取ります。tryEmit()は非ブロッキングで、バッファが満杯の場合はfalseを返します。高負荷システムの場合は、DROP_OLDESTとともにtryEmit()を使用してください。これにより、スレッドのブロッキングが防止されます。
replay=1なしのSharedFlowは最後の値を保存しません。画面回転時に新しいサブスクライバは現在の状態を受け取らず、UIは空のままになります。replay=1の場合、SharedFlowはStateFlowのように動作しますが、equals()による比較の最適化が失われ、同じ値が再発行されたときに不要な通知が発生します。StateFlowは状態に適した選択肢であり、SharedFlowはイベント用です。
SharedFlowのテストには、Flowテスト用のKotlinライブラリであるTurbineを使用してください。Turbineを使用すると、タイムアウトと完了確認を使用して各発行を個別に確認できます。例:viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }。runTestで予想イベント数を指定して.toList()を使用することもできます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。