SharedFlow: その概要、AndroidにおけるSharedFlow vs StateFlow

著者: IT Sectr 公開日: 2026-02-20 読了時間: 9 分

SharedFlow はKotlin Coroutinesライブラリのホットリアクティブフローであり、画面回転やサブスクライバの再作成時に繰り返されるべきではない単発イベント(one-shot events)向けに最適化されています。SharedFlowがStateFlowとどのように異なるかを示します。StateFlowとは異なり、SharedFlowは新しいサブスクライバのために最後の値を保存せず、replay、extraBufferCapacity、onBufferOverflowの設定をサポートします。Google(Android Developers、2025)によると、SharedFlowはナビゲーションコマンド、Snackbarメッセージ、および正確に1回処理されるべきその他のイベントに推奨されるソリューションです。

重要ポイント

  • SharedFlow — 単発イベント用のホットフロー:新しいサブスクライバはreplay設定なしでは以前の値を受け取りません。
  • MutableSharedFlow — イベント送信用のemit()およびtryEmit()メソッドを持つ可変バージョン。
  • SharedFlow vs StateFlow: SharedFlowは値を結合せず(複数をバッファリング可能)、初期値を必要とせず、単発イベントに適しています。
  • replay — 新しいサブスクライバに再生される最近のイベント数(デフォルト0)。
  • extraBufferCapacity — replayを超えるイベント用の追加バッファ。emit()のブロックを防ぎます。

KotlinにおけるSharedFlowとは?

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 vs StateFlow: それぞれの使用場面

SharedFlowとStateFlowの選択は、転送されるデータのセマンティクス(状態はStateFlow、イベントはSharedFlow)に依存します。以下に、例を示しながら明確な基準を示します。

基準SharedFlowStateFlow
セマンティクス単発イベント(ナビゲーション、トースト、アラート)UI状態(リスト、ローディング、エラー)
初期値不要必須
サブスクリプション時の再生replay > 0の場合のみ常に最後の値
結合なし — イベントは失われません(バッファが満杯でない場合)あり — 最新のみ保存
バッファリングreplay + extraBufferCapacityで設定可能1のみ(replay=1固定)
使用法navigationEvent、showSnackbar、openDialogitems、isLoading、uiState

最も単純なルール:画面回転時にデータを表示する必要がある場合は状態(StateFlow)。画面回転時にイベントを繰り返してはならない場合は単発イベント(SharedFlow)。たとえば、エラーメッセージのトーストはSharedFlow:回転時にトーストを再度表示してはいけません。商品リストはStateFlow:回転時にリストは画面に残る必要があります。

IT Sectrでは、SharedFlowを次の用途で使用しています:ナビゲーションコマンド(画面遷移、ディープリンク開封)、UIイベント(Snackbar、AlertDialog)、システム通知(バックグラウンドデータ更新、支払い結果)、分析イベント(ロギング、トラッキング)。

MutableSharedFlow: emit、tryEmit、バッファリング

MutableSharedFlow はSharedFlowの可変バージョンで、イベント送信用にemit()(suspend)およびtryEmit()(非suspend)メソッドを提供します。emit()は、バッファが満杯でonBufferOverflow = SUSPENDの場合に一時停止します。tryEmit()は、イベントがバッファに正常に追加されたかどうかを示すBooleanを返します。

kotlin
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以上のイベントを処理します。

単発イベントのためのSharedFlow: Eventパターン

Eventパターン(またはUiEvent) は、ViewModelからViewに単発イベントを渡すためのGoogle推奨の方法です。状態(StateFlow)とは異なり、イベントは正確に1回処理されるべきであり、画面回転時に繰り返されてはいけません。replay = 0のSharedFlowはこのタスクに最適です。

kotlin
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回のみ発生し、回転のたびに発生しないことが保証されます。

SharedFlowのパラメータ: replay、extraBufferCapacity、onBufferOverflow

MutableSharedFlowのコンストラクタは、バッファの動作を決定する3つのパラメータを受け入れます。誤った設定は、イベントの損失やemit()のブロックにつながる可能性があります。

パラメータデフォルト説明
replayInt0新しいサブスクライバに再生される最近のイベント数。0 = 再生しない、1 = StateFlowのように
extraBufferCapacityInt0replayを超える追加バッファ。イベントは循環バッファに保存されます。ほとんどのシナリオで推奨される上限は64
onBufferOverflowBufferOverflowSUSPENDバッファ満杯時の戦略:SUSPEND、DROP_OLDEST、DROP_LATEST
kotlin
// さまざまなシナリオの設定:

// 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を使用してください。これにより、送信者をブロックする代償として、イベントが失われないことが保証されます。

コード例: KotlinでのSharedFlow

例1: Jetpack Navigationを使用したナビゲーションのSharedFlow

ナビゲーションコマンドは、SharedFlowの古典的なユースケースです。Fragmentがイベントをサブスクライブし、ナビゲーションを実行します。画面回転時にコマンドは繰り返されません。

kotlin
// 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()
            }
        }
    }
}

例2: RoomとFlowオペレータを使用したSharedFlow

複雑なシナリオ:UI用のStateFlowと組み合わせたバックグラウンドイベント通知用のSharedFlow。

kotlin
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推奨のパターンです。

よくある質問

SharedFlowはイベントを失う可能性がありますか?

はい、バッファが満杯でonBufferOverflow = DROP_OLDESTまたはDROP_LATESTの場合に発生します。SharedFlowはすべてのイベントの配信を保証しません。これはメッセージキュー(Channelなど)ではありません。すべてのイベントの確実な配信が必要な場合は、無制限バッファ(UNLIMITED)またはBroadcastChannel(非推奨)のChannelを使用してください。UIイベントの場合、古いイベント(古いナビゲーションなど)の損失は期待される動作であり、バグではありません。

SharedFlowはChannelとどう違いますか?

Channel はFIFOキューであり、各イベントは正確に1つのサブスクライバに配信されます(ポイントツーポイント)。SharedFlowはブロードキャストであり、各イベントはすべてのアクティブなサブスクライバに配信されます。SharedFlowはBroadcastChannel(非推奨)に近く、1対多のシナリオに適しています。Channelは1対1(スレッドプール、パイプライン)用です。JetBrainsの推奨によると、SharedFlowはすべての新しいプロジェクトでBroadcastChannelの代替です。

SharedFlowをスレッドセーフにするには?

SharedFlowはすでにスレッドセーフです。emit()とcollect()は適切に同期されています。複数のスレッドがロックなしでemit()を呼び出すことができ、すべてのアクティブなサブスクライバは正しい順序でイベントを受け取ります。tryEmit()は非ブロッキングで、バッファが満杯の場合はfalseを返します。高負荷システムの場合は、DROP_OLDESTとともにtryEmit()を使用してください。これにより、スレッドのブロッキングが防止されます。

なぜSharedFlowは状態に使用されないのですか?

replay=1なしのSharedFlowは最後の値を保存しません。画面回転時に新しいサブスクライバは現在の状態を受け取らず、UIは空のままになります。replay=1の場合、SharedFlowはStateFlowのように動作しますが、equals()による比較の最適化が失われ、同じ値が再発行されたときに不要な通知が発生します。StateFlowは状態に適した選択肢であり、SharedFlowはイベント用です。

SharedFlowをテストするには?

SharedFlowのテストには、Flowテスト用のKotlinライブラリであるTurbineを使用してください。Turbineを使用すると、タイムアウトと完了確認を使用して各発行を個別に確認できます。例:viewModel.event.test { assertEquals(UiEvent.ShowSnackbar("OK"), awaitItem()) }。runTestで予想イベント数を指定して.toList()を使用することもできます。

まとめ

  • SharedFlow — 最後の状態に結びつかない単発イベント用のホットリアクティブフロー。
  • SharedFlow vs StateFlow: SharedFlowはイベント(ナビゲーション、トースト、アラート)用、StateFlowは状態(リスト、ローディング、エラー)用。
  • MutableSharedFlow(replay=0、extraBufferCapacity=5、DROP_OLDEST)はUIイベントの標準設定です。
  • emit() — ブロッキング発行用のsuspend関数。tryEmit() — Boolean結果を返す非suspend。
  • sealed classを使用したUiEventパターンは、ViewModelからViewに単発イベントを渡すためのGoogle推奨の方法です。
  • SharedFlowは各サブスクライバへの配信を保証しますが、バッファオーバーフロー時の各イベントの配信は保証しません
  • 単一のViewModelにおけるSharedFlow + StateFlowの組み合わせは、状態とイベントを分離するアーキテクチャの最適なパターンです。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください