Kotlin Flow vs StateFlow vs SharedFlow:2026年のAndroid面接質問

2026年にAndroidの面接官が問う Kotlin Flow・StateFlow・SharedFlow の質問を、明快な回答・比較表・本番投入できるコードとともに解説します。

Android面接に向けた Kotlin Flow・StateFlow・SharedFlow の比較

Kotlin Flow、StateFlow、SharedFlow の違いは、2026 年の Android 面接で最も頻出するテーマの一つです。この 3 つを混同すると、コルーチンに関する質問であっさり評価を落としかねません。StateFlow と SharedFlow はどちらも Flow の上に構築されたホットなストリームですが、解決する課題は異なります。前者は状態を保持するため、後者はイベントを配信するために存在します。以下では、Android の面接官が実際に投げかける質問を取り上げ、正確な回答と本番投入できるコードとともに解説します。

20秒で答えるポイント

Flow はコールドで、コレクターごとにプロデューサーを一度実行します。StateFlow はホットで conflate されるストリームであり、常に単一の現在値を保持するため UI 状態に最適です。SharedFlow は初期値を必須としないホットなストリームで、画面遷移やスナックバーのような一度きりのイベントに向いています。

Kotlin Flow、StateFlow、SharedFlow:核心となる違い

StateFlow と SharedFlow は SharedFlow を特殊化したものであり、その SharedFlow 自体がホットな Flow です。面接官が確かめようとするのは、コールドとホットの違い、現在値を保持するかどうか、そして重複した値の発行時にそれぞれがどう振る舞うか、という点です。簡潔なメンタルモデルとしては、Flow はレシピ、StateFlow は単一の可変値、SharedFlow はイベントバス、と捉えるとよいでしょう。

| プロパティ | Flow(コールド) | StateFlow | SharedFlow | |---|---|---|---| | 温度 | コールド | ホット | ホット | | 初期値 | なし | 必須 | 任意(replay で指定) | | 現在値を保持 | いいえ | はい(.value 経由) | いいえ | | 重複を発行 | はい | いいえ(conflate + 重複排除) | 設定可能 | | コレクター追加時 | 毎回新しいストリーム | 共有 | 共有 | | 最適な用途 | 非同期データパイプライン | UI 状態 | 一度きりのイベント |

Kotlin コルーチンの公式ドキュメント は、このデフォルトでコールドという振る舞いを Flow を特徴づける性質として扱っています。強い回答はここから始まります。

なぜ Kotlin の Flow はデフォルトでコールドなのか

コールドな flow は collect() が呼ばれるまで何もせず、コレクターごとにプロデューサーブロックを再実行します。同じコールドな flow に対して 2 つのコレクターがあれば、独立した 2 回のネットワーク呼び出しが発生します。これは最も出題される概念なので、短い例で回答を裏づけましょう。

SearchRepository.ktkotlin
fun searchResults(query: String): Flow<List<Result>> = flow {
    // This block runs fresh for every collector.
    // Nothing executes until a collector calls collect().
    val results = api.search(query) // suspending network call
    emit(results)                   // pushed downstream to the collector
}

プロデューサーがコレクターごとに再起動するため、コールドな flow はオンデマンドで実行すべきデータパイプラインのデフォルトとして適しています。問題になるのは、ある値を画面全体で共有する必要がある場合だけであり、まさにそこで StateFlow と SharedFlow の出番となります。

StateFlow の面接質問:常に値を発行する状態

StateFlow は常に値を持つホットな flow で、その最新値を新しいコレクターすべてにリプレイします。初期値が必須で、同期的な読み取りのために .value を公開し、発行を conflate します。つまり、高速に連続する更新では中間の値がスキップされることがあり、同じ値を 2 回設定しても等価性で重複排除されるため何も発行されません。これにより、ViewModel における LiveData の現代的な置き換えとなっています。

SearchViewModel.ktkotlin
class SearchViewModel(private val repo: SearchRepository) : ViewModel() {
    // MutableStateFlow demands an initial value, so the UI always has something to render.
    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Idle)
    val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()

    fun onQueryChanged(query: String) {
        _uiState.value = SearchUiState.Loading
        viewModelScope.launch {
            val results = repo.search(query)
            // update {} applies the change atomically, safe under concurrent callers.
            _uiState.update { SearchUiState.Success(results) }
        }
    }
}

よくある追加質問は、可変フィールドではなく asStateFlow() を公開する理由です。これは UI に読み取り専用のビューを渡すことで、状態を ViewModel 経由でしか変更できないようにし、単方向データフローを保ちます。Android 公式の StateFlow と SharedFlow のガイド も、まさにこのパターンを推奨しています。

SharedFlow と StateFlow:イベントに適した型の選び方

典型的なひっかけ質問は「StateFlow は一度きりのイベントを配信できるか」というものです。正直な答えは「安全にはできない」です。StateFlow は conflate と重複排除を行うため、急速な更新のもとでは画面遷移イベントが取りこぼされる可能性があり、さらに構成変更時には新しいコレクターが保持された値をリプレイするため、イベントが再発火してしまいます。replay = 0 を指定した SharedFlow は両方を解決します。何も保持されず、各発行は発行時にアクティブなコレクターだけに届きます。

CheckoutViewModel.ktkotlin
class CheckoutViewModel : ViewModel() {
    // replay = 0: a late collector must NOT re-receive a past navigation event.
    private val _events = MutableSharedFlow<CheckoutEvent>(replay = 0)
    val events: SharedFlow<CheckoutEvent> = _events.asSharedFlow()

    fun onPaymentConfirmed() {
        viewModelScope.launch {
            // emit() suspends if the buffer is full; tryEmit() is the non-suspending variant.
            _events.emit(CheckoutEvent.NavigateToReceipt)
        }
    }
}

アーキテクチャに関する質問では、状態とイベントを分離する背景にあるより深い理由が MVVM と MVI の比較 に現れます。そこでは MVI がイベントを可変状態ではなく明示的なストリームとして扱っています。

stateIn でコールドな Flow をホットな StateFlow へ変換する

面接官は、リポジトリのコールドな flow を UI 状態に変える方法をよく尋ねます。答えは stateIn(単一の保持値の場合)または shareIn(現在値を持たないブロードキャストの場合)です。SharingStarted パラメーターは上流をいつアクティブにするかを制御し、WhileSubscribed(5_000) が標準的な選択肢です。画面回転をまたいで flow を生かし続けつつ、画面を閉じたときにはリークさせないからです。

ProfileViewModel.ktkotlin
val profile: StateFlow<Profile> = repo.profileStream()
    .stateIn(
        scope = viewModelScope,
        // Keep the upstream alive 5s after the last collector leaves, surviving rotation.
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = Profile.EMPTY
    )

stateInshareIn の違いは、まさに StateFlow と SharedFlow の違いそのものです。stateIninitialValue を必要とし状態を保持しますが、shareInreplay の数を受け取ってブロードキャストします。より広いコルーチンのモデルを Kotlin コルーチンを使いこなすためのこのガイド で振り返ると、これらの演算子をスコープやキャンセルと結びつけて理解できます。

Androidの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Android のライフサイクル上で flow を安全に収集する

シニア級の質問は、画面がバックグラウンドにある間に処理をリークさせずに flow を収集する方法です。単純な launch の中で収集すると、UI が停止しても実行され続け、CPU を無駄にしクラッシュのリスクも生みます。repeatOnLifecycle(STARTED)STOP で収集をキャンセルし、START で再開します。

ProfileFragment.kt (View system)kotlin
viewLifecycleOwner.lifecycleScope.launch {
    // Collection restarts on STARTED and cancels on STOPPED: no wasted work in the background.
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state -> render(state) }
    }
}

Jetpack Compose での対応物は collectAsStateWithLifecycle() で、アプリがバックグラウンドに入ると収集を停止し、復帰時に再開します。

ProfileScreen.kt (Jetpack Compose)kotlin
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
    // collectAsStateWithLifecycle stops collecting when the app is backgrounded.
    val state by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(state)
}

このライフサイクル対応こそが、collectAsStateWithLifecycle と組み合わせた StateFlow が現代の Compose アプリにおける標準的な状態パターンである理由です。この点は Jetpack Compose の面接質問 でさらに詳しく扱っています。

StateFlow と SharedFlow の発行をテストする方法

テストは、シニア候補者が差をつける場面です。素朴な方法は stateFlow.value を一度読むだけですが、それでは中間状態を見逃し、SharedFlow はまったく観測できません。標準的な答えは Turbine ライブラリを挙げることです。Turbine は各発行が届くまで suspend し、期待した値が来なければテストを失敗させます。

SearchViewModelTest.ktkotlin
@Test
fun `emits Loading then Success on query`() = runTest {
    val viewModel = SearchViewModel(fakeRepo)

    viewModel.uiState.test {
        assertEquals(SearchUiState.Idle, awaitItem())     // initial value
        viewModel.onQueryChanged("kotlin")
        assertEquals(SearchUiState.Loading, awaitItem())  // intermediate state
        assertEquals(SearchUiState.Success(fakeResults), awaitItem())
        cancelAndIgnoreRemainingEvents()
    }
}

Turbine を runTest とテスト用ディスパッチャーと組み合わせるとアサーションが決定的になり、StandardTestDispatcherUnconfinedTestDispatcher の違いに触れれば、実際のコルーチンテストの経験を示せます。

よくある Kotlin Flow の追加質問

面接官は矢継ぎ早の確認で締めくくります。SharingStarted.WhileSubscribed(5_000) は何をするのでしょうか。最初のサブスクライバーで上流を開始し、最後のサブスクライバーが解除してから 5 秒後に停止します。これは画面回転を乗り切るのに十分な長さです。なぜ StateFlow は重複した値をスキップするのでしょうか。equals() で比較するため、等価な値を発行しても何も起きず、だからこそ状態には data class が重要になります。SharedFlow は StateFlow のように振る舞えるのでしょうか。replay = 1 を設定すれば最後の値を保持しますが、それでも同期的な .value は持たず、重複排除も一切行いません。SharedFlow のバックプレッシャーは何が制御するのでしょうか。extraBufferCapacityonBufferOverflow のパラメーターで、イベントには BufferOverflow.DROP_OLDEST がよく選ばれます。これらの演算子のより深い演習は Kotlin コルーチンと Flow の面接モジュール にあります。

候補者を不合格にする間違い

画面遷移やスナックバーのイベントに StateFlow を使うことです。最後の値を新しいコレクターにリプレイするため、画面回転後にイベントが再発火し、ユーザーは 2 回遷移させられてしまいます。replay = 0SharedFlow を使うか、処理後にクリアされる一度きりのイベントとしてモデル化しましょう。

kotlinx.coroutines のソースと、その GitHub 上の flow パッケージリファレンス は面接前に目を通す価値があります。StateFlowSharedFlow の KDoc が conflate とリプレイの保証を正確に述べているからです。

まとめ

Kotlin Flow、StateFlow、SharedFlow の区別は、面接で正確な言葉づかいをすると評価されます。持ち込むべき要点は次のとおりです。

  • Flow はコールドで、プロデューサーがコレクターごとに再実行されるため、オンデマンドのデータパイプラインに向いています。
  • StateFlow はホットで常に値を保持し、conflate と重複排除を行うため、UI 状態のためのツールです。
  • SharedFlow はホットで初期値を必須とせず、一度きりのイベントに適した選択肢です。
  • stateIn または shareIn でコールドからホットへ変換し、画面回転を乗り切るために SharingStarted.WhileSubscribed(5_000) を使います。
  • repeatOnLifecycle(STARTED) または collectAsStateWithLifecycle() で収集し、バックグラウンドでの処理とリークを避けます。
  • イベントを StateFlow で配信してはいけません。そのリプレイの挙動により、構成変更後にイベントが再発火します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#android
#kotlin
#coroutines
#flow
#interview

共有

関連記事