0Pricing
Kotlin Academy · レッスン

shareInとstateInでCold FlowをHotに変換する

共有演算子を使ってCold FlowをHotストリームに変換します。

「shareInとstateInでCold FlowをHotに変換する」はCoddyKit上の無料Kotlin Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはKotlin Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Kotlin Academyコースには全4レッスンが含まれています。

Cold FlowとHot Flowの違い

Cold Flowはコレクターごとに再起動しますが、Hot Flowは1つの上流サブスクリプションを共有します。Cold FlowをHot Flowに変換すると、複数のコレクターが購読したときの重複処理を避けられます。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun expensiveFlow() = flow {
    println("Starting upstream work")  // runs once if shared
    repeat(3) { delay(100); emit(it) }
}
fun main() = runBlocking {
    val cold = expensiveFlow()
    // Two collectors = two executions:
    launch { cold.collect { } }
    launch { cold.collect { } }
    delay(500)
    coroutineContext.cancelChildren()
}

shareInの基本

flow.shareIn(scope, started, replay)はCold FlowをSharedFlowに変換し、1つの上流サブスクリプションを共有します。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = flow {
        println("Upstream started once")
        repeat(5) { delay(100); emit(it) }
    }
    val hot = cold.shareIn(this, SharingStarted.Eagerly, replay = 0)
    launch { hot.collect { println("A: $it") } }
    launch { hot.collect { println("B: $it") } }
    delay(600)
    coroutineContext.cancelChildren()
}

stateInの基本

flow.stateIn(scope, started, initialValue)はStateFlowに変換します。StateFlowは常に値を持ち、1つの値をリプレイします。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = flow { delay(100); emit(42) }
    val state: StateFlow<Int> = cold.stateIn(
        scope = this,
        started = SharingStarted.Eagerly,
        initialValue = 0
    )
    println(state.value)  // 0 immediately
    delay(200)
    println(state.value)  // 42 after upstream emits
    coroutineContext.cancelChildren()
}

SharingStarted.Eagerly

Eagerlyでは、サブスクライバーの有無にかかわらず、shareIn/stateInが呼び出された時点ですぐに上流が開始されます。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Eagerly started")
        emit(1)
    }.shareIn(this, SharingStarted.Eagerly)
    // Upstream already running even before any collect
    delay(50)
    launch { flow.collect { println(it) } }
    delay(100)
    coroutineContext.cancelChildren()
}

SharingStarted.Lazily

Lazilyでは、最初のサブスクライバーが購読した時点で上流が開始され、その後はサブスクライバーが0になっても停止しません。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Lazily started on first subscriber")
        repeat(3) { delay(100); emit(it) }
    }.shareIn(this, SharingStarted.Lazily, replay = 1)
    delay(50)  // no subscriber yet — not started
    launch { flow.collect { println(it) } }  // triggers start
    delay(400)
    coroutineContext.cancelChildren()
}

SharingStarted.WhileSubscribed

WhileSubscribed(stopTimeout)では、最初のサブスクライバーが参加すると上流が開始され、最後のサブスクライバーが離れてからstopTimeoutミリ秒後に停止します。ViewModelに最適です。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val flow = flow {
        println("Started")
        repeat(10) { delay(100); emit(it) }
    }.shareIn(this, SharingStarted.WhileSubscribed(500))
    val job = launch { flow.collect { print("$it ") } }
    delay(300)
    job.cancel()    // subscriber left
    delay(200)      // within 500ms stop timeout — still running
    launch { flow.collect { print("resume $it ") } }
    delay(500)
    coroutineContext.cancelChildren()
}

一般的なViewModelパターン

AndroidのViewModelでは、stateIn(viewModelScope, WhileSubscribed(5000), initialValue)を使ってリポジトリのFlowを変換すると、共有しながら構成変更後も処理を継続できます。

import kotlinx.coroutines.flow.*
// In ViewModel:
// val uiState: StateFlow<UiState> = repository
//     .dataFlow()
//     .map { UiState.Success(it) }
//     .stateIn(
//         scope = viewModelScope,
//         started = SharingStarted.WhileSubscribed(5_000),
//         initialValue = UiState.Loading
//     )
fun main() { println("WhileSubscribed(5000) is the recommended ViewModel pattern") }

shareInとstateInの比較

shareIn → SharedFlow(現在値なし、リプレイ数を設定可能)。stateIn → StateFlow(常に値を持ち、replay=1)。利用側が現在の状態を必要とするかどうかに基づいて選択します。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
    val cold = (1..3).asFlow().map { it * 10 }

    // SharedFlow — no initial value:
    val shared: SharedFlow<Int> = cold.shareIn(this, SharingStarted.Eagerly, replay = 1)

    // StateFlow — always has a value:
    val state: StateFlow<Int> = cold.stateIn(this, SharingStarted.Eagerly, 0)

    delay(100)
    println("shared cache: ${shared.replayCache}")
    println("state value: ${state.value}")
    coroutineContext.cancelChildren()
}

Replayキャッシュのトレードオフ

replayの値を大きくすると、後から購読したサブスクライバーがより多くの履歴を受け取れる一方で、より多くのメモリを使用します。UIの状態にはreplay=1(stateIn)で十分です。イベントログでは、より大きなreplayが必要になる場合があります。

import kotlinx.coroutines.flow.*
// replay=0: no history, only future events
// replay=1: last value (equivalent to stateIn)
// replay=N: last N events — use for message feeds, logs

// Trade-off: memory vs subscriber freshness
fun main() { println("Choose replay based on late-subscriber requirements") }

共有しないFlowのリスク

共有しない場合、Composeの各コレクターやViewModelの各オブザーバーが上流処理を再実行するため、ネットワーク呼び出しやデータベースクエリが重複します。負荷の高いFlowは必ず共有してください。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun expensiveApi() = flow {
    println("API CALL")  // without sharing: once per collector
    emit("data")
}
fun main() = runBlocking {
    val shared = expensiveApi().shareIn(this, SharingStarted.Lazily, replay = 1)
    launch { shared.collect { } }  // one API call, shared
    launch { shared.collect { } }  // same emission
    delay(200)
    coroutineContext.cancelChildren()
}

共有時のリソース解放

共有Flowのスコープがキャンセルされると、上流のFlowもキャンセルされ、そのfinallyブロックが実行されます。これにより、リソースが適切に解放されます。

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun resourceFlow() = flow {
    try { repeat(10) { delay(100); emit(it) } }
    finally { println("Upstream cleaned up") }
}
fun main() = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    val shared = resourceFlow().shareIn(scope, SharingStarted.Eagerly)
    launch { shared.collect { print("$it ") } }
    delay(250)
    scope.cancel()  // upstream cleanup runs
    delay(100)
}

確認問題

AndroidのViewModelには、どのSharingStarted戦略が推奨されますか。

まとめ

shareInはSharedFlowに変換し、stateInはStateFlowに変換します。ViewModelではWhileSubscribedを使用してリソースを節約します。共有すると、複数のコレクターが購読したときに上流処理が重複して実行されるのを防げます。

よくある質問

「shareInとstateInでCold FlowをHotに変換する」レッスンは無料ですか?

はい。「shareInとstateInでCold FlowをHotに変換する」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Kotlin Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Kotlin Academyコースには全4レッスンが含まれています。

「shareInとstateInでCold FlowをHotに変換する」で何を学びますか?

共有演算子を使ってCold FlowをHotストリームに変換します。 ブラウザで直接実行するハンズオンコードでKotlin Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Kotlin Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのKotlin Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「shareInとstateInでCold FlowをHotに変換する」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このKotlin Academyレッスンでコードを書いて実行できますか?

はい。すべてのKotlin Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. StateFlow:UI向けのHotな状態保持
  2. SharedFlow:イベントバスとワンショットイベント
  3. shareInとstateInでCold FlowをHotに変換する
  4. TurbineによるStateFlowとSharedFlowのテスト
← Kotlin Academyに戻る