- Kotlin
Kotlin Flowの使い方 StateFlowとSharedFlowの違い
Kotlin の Flow とは、時間をおいて順番に届く複数の値を、コルーチンの上で扱うための型です。 flow { } で作る Flow は「collect されるたびに最初から流れるコールドフロー」、StateFlow と SharedFlow は「購読者がいなくても値が流れ続けるホットフロー」で、StateFlow は最新の値を1つ持つ状態の入れ物、SharedFlow は起きた出来事を配る放送と考えると選びやすくなります。
この記事のコードはすべて runBlocking だけで動くコンソールプログラムです。画面のフレームワークを使わずに、3種類の違いをタイムスタンプ付きの出力で比べ、演算子の一覧、stateIn / shareIn の started の違い、collect の次の行に進まない現象と launchIn での書き分けまで、実際の出力を添えて説明します。
確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10 / kotlinx-coroutines-core 1.8.0。掲載した出力はすべてこの環境で実行したものです(ミリ秒の値は実行のたびに少し変わります)。
Flow・StateFlow・SharedFlow の早見表
| 比べる点 | Flow(flow { } で作る) |
SharedFlow | StateFlow |
|---|---|---|---|
| コールドかホットか | コールド(collect されて初めて動く) | ホット(購読者と関係なく値が流れる) | ホット |
| 初期値 | なし | なし | 必須(MutableStateFlow(0)) |
| 購読前の値(replay) | collect のたびに最初から全部流れる | replay に指定した個数だけ届く |
最新の1個が必ず届く |
| 同じ値が続いたとき | そのまま流す | そのまま流す | 間引く(equals で同じなら流さない) |
| 完了するか | ブロックの終わりで完了する | 完了しない(collect は cancel まで続く) | 完了しない |
| 今の値を読む | できない | replayCache で replay 分を読める |
value で読める |
| 値を入れる側 | emit(ブロックの中だけ) |
emit / tryEmit |
value = ... / update { } |
| 購読者が複数のとき | 購読者ごとに別々に実行 | 同じ値を全員に配る | 同じ値を全員に配る |
| 向いている用途 | 取得処理・計算の流れ | 通知・イベント | 状態(今の値) |
迷ったときの選び方:「今の値」が意味を持つなら StateFlow、「起きたこと」を1回ずつ伝えたいなら SharedFlow、呼ばれるたびに処理をやり直したいなら flow { } です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Flowの使い方 StateFlowとSharedFlowの違い」
準備:依存の追加と実行コマンド
Flow は標準ライブラリではなく kotlinx.coroutines に入っています。Gradle(Kotlin DSL)では、公式リポジトリの README にある次の1行を追加します(README に記載の最新版は 1.11.0 です)。
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
この記事では Gradle を使わず、kotlinc に jar を直接渡して実行しました。-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 にそろえるための指定です。
kotlinc Basic.kt -cp kotlinx-coroutines-core-jvm.jar -include-runtime -d basic.jar
java -Dstdout.encoding=UTF-8 -cp basic.jar:kotlinx-coroutines-core-jvm.jar BasicKt
flow ビルダーと collect:まず動かす
flow { } の中で emit(値) を呼ぶと値が1つ流れ、受け取る側は collect { } で1つずつ処理します。map や filter などの演算子は、collect の前にいくつでもつなげられます。
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
fun numbers(): Flow<Int> = flow {
println(" flow 開始")
for (i in 1..5) {
delay(100)
emit(i)
}
}
fun main() = runBlocking {
println("1) collect でそのまま受け取る")
numbers().collect { println(" 受信 $it") }
println("2) 演算子をつなぐ")
numbers()
.filter { it % 2 == 1 }
.map { it * 10 }
.onEach { println(" onEach $it") }
.take(2)
.collect { println(" 受信 $it") }
println("3) 終端演算子で値にする")
println(" toList = " + numbers().toList())
println(" first = " + numbers().first())
println(" fold = " + numbers().fold(0) { acc, v -> acc + v })
println("4) ほかの作り方")
println(" flowOf = " + flowOf("a", "b").toList())
println(" asFlow = " + listOf(1, 2, 3).asFlow().map { it * it }.toList())
}
1) collect でそのまま受け取る
flow 開始
受信 1
受信 2
受信 3
受信 4
受信 5
2) 演算子をつなぐ
flow 開始
onEach 10
受信 10
onEach 30
受信 30
3) 終端演算子で値にする
flow 開始
toList = [1, 2, 3, 4, 5]
flow 開始
first = 1
flow 開始
fold = 15
4) ほかの作り方
flowOf = [a, b]
asFlow = [1, 4, 9]
読み取れることは3つあります。
flow 開始が collect・toList・first のたびに表示されている:Flow は呼ばれるたびに最初から実行されます。take(2)は2個受け取った時点で上流を止める:3個目以降の emit は実行されません。collect・toList・first・foldは suspend 関数:これらを終端演算子と呼び、呼んだ時点で初めて Flow が動きます。mapやfilterは Flow を返すだけで、それだけでは何も実行されません。
コールドフロー:collect のたびに最初から流れる
Flow を作ってから 300 ミリ秒待ち、A と B が順番に collect します。行頭の数字はプログラム開始からの経過ミリ秒です。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
val t0 = System.currentTimeMillis()
fun log(msg: String) = println("[%4d ms] %s".format(System.currentTimeMillis() - t0, msg))
fun main() = runBlocking {
val cold = flow {
log("flow ブロック開始")
for (i in 1..3) {
delay(100)
emit(i)
}
}
log("flow を作っただけ(まだ何も流れない)")
delay(300)
log("A が collect")
cold.collect { log("A 受信 $it") }
log("B が collect")
cold.collect { log("B 受信 $it") }
}
[ 104 ms] flow を作っただけ(まだ何も流れない)
[ 439 ms] A が collect
[ 447 ms] flow ブロック開始
[ 591 ms] A 受信 1
[ 691 ms] A 受信 2
[ 792 ms] A 受信 3
[ 793 ms] B が collect
[ 795 ms] flow ブロック開始
[ 895 ms] B 受信 1
[ 996 ms] B 受信 2
[1096 ms] B 受信 3
作っただけの 300 ミリ秒間は何も起きず、A が collect した 447 ms に初めてブロックが動いています。 B が collect すると、もう一度 flow ブロック開始 が出て 1 から流れ直します。A が受け取り終わったあとに来た B も、1・2・3 をすべて受け取れます。これがコールドフローです。公式ドキュメントも、コールドフローのブロックは collect されるまで動かず、新しい collect ごとに新しい実行が始まると説明しています。
ホットフロー:StateFlow と SharedFlow は購読前の値を replay 分だけ受け取る
同じ流れを SharedFlow と StateFlow で試します。早い購読者は最初から、遅い購読者は 1・2・3 が流れたあとに collect を始めます。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
val t0 = System.currentTimeMillis()
fun log(msg: String) = println("[%4d ms] %s".format(System.currentTimeMillis() - t0, msg))
suspend fun demo(name: String, shared: MutableSharedFlow<Int>) = coroutineScope {
log("--- $name ---")
val early = launch { shared.collect { log(" 早い購読者 受信 $it") } }
delay(50)
for (i in 1..3) {
log("emit $i")
shared.emit(i)
delay(100)
}
val late = launch { shared.collect { log(" 遅い購読者 受信 $it") } }
delay(50)
log("emit 4")
shared.emit(4)
delay(100)
early.cancel()
late.cancel()
}
fun main() = runBlocking {
demo("SharedFlow replay=0", MutableSharedFlow(replay = 0))
demo("SharedFlow replay=2", MutableSharedFlow(replay = 2))
log("--- StateFlow 初期値 0 ---")
val state = MutableStateFlow(0)
val early = launch { state.collect { log(" 早い購読者 受信 $it") } }
delay(50)
for (i in 1..3) {
log("value = $i")
state.value = i
delay(100)
}
val late = launch { state.collect { log(" 遅い購読者 受信 $it") } }
delay(50)
log("value = 4")
state.value = 4
delay(100)
log("今の値 state.value = ${state.value}")
early.cancel()
late.cancel()
}
[ 156 ms] --- SharedFlow replay=0 ---
[ 266 ms] emit 1
[ 268 ms] 早い購読者 受信 1
[ 369 ms] emit 2
[ 369 ms] 早い購読者 受信 2
[ 469 ms] emit 3
[ 470 ms] 早い購読者 受信 3
[ 627 ms] emit 4
[ 628 ms] 早い購読者 受信 4
[ 628 ms] 遅い購読者 受信 4
[ 732 ms] --- SharedFlow replay=2 ---
[ 782 ms] emit 1
[ 783 ms] 早い購読者 受信 1
[ 887 ms] emit 2
[ 887 ms] 早い購読者 受信 2
[ 987 ms] emit 3
[ 987 ms] 早い購読者 受信 3
[1088 ms] 遅い購読者 受信 2
[1088 ms] 遅い購読者 受信 3
[1138 ms] emit 4
[1139 ms] 早い購読者 受信 4
[1139 ms] 遅い購読者 受信 4
[1240 ms] --- StateFlow 初期値 0 ---
[1243 ms] 早い購読者 受信 0
[1292 ms] value = 1
[1292 ms] 早い購読者 受信 1
[1393 ms] value = 2
[1393 ms] 早い購読者 受信 2
[1494 ms] value = 3
[1494 ms] 早い購読者 受信 3
[1595 ms] 遅い購読者 受信 3
[1645 ms] value = 4
[1646 ms] 早い購読者 受信 4
[1646 ms] 遅い購読者 受信 4
[1746 ms] 今の値 state.value = 4
遅い購読者が受け取った値だけを抜き出すと、違いがはっきりします。
| 種類 | 遅い購読者が受け取った値 | 理由 |
|---|---|---|
flow { }(前の節) |
1, 2, 3 すべて | collect のたびに最初から実行される |
SharedFlow replay = 0 |
4 だけ | 購読前の 1・2・3 は届かない |
SharedFlow replay = 2 |
2, 3, 4 | 直近2個が購読した瞬間(1088 ms)に届く |
| StateFlow | 3, 4 | 最新の1個(3)が購読した瞬間(1595 ms)に届く |
ホットフローでは、値は購読者の有無に関係なく流れ、あとから来た購読者に届くのは replay の分だけです。 StateFlow は初期値を持つので、早い購読者も collect した直後(1243 ms)に 0 を受け取っています。
StateFlow は同じ値を間引き、SharedFlow はそのまま流す
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val state = MutableStateFlow("A")
val shared = MutableSharedFlow<String>()
val j1 = launch { state.collect { println("StateFlow 受信 $it") } }
val j2 = launch { shared.collect { println("SharedFlow 受信 $it") } }
delay(50)
for (v in listOf("A", "B", "B", "C")) {
println("送る $v")
state.value = v
shared.emit(v)
delay(50)
}
println("--- 待たずに続けて代入 ---")
state.value = "X"
state.value = "Y"
state.value = "Z"
delay(50)
j1.cancel()
j2.cancel()
}
StateFlow 受信 A
送る A
SharedFlow 受信 A
送る B
StateFlow 受信 B
SharedFlow 受信 B
送る B
SharedFlow 受信 B
送る C
StateFlow 受信 C
SharedFlow 受信 C
--- 待たずに続けて代入 ---
StateFlow 受信 Z
- 1行目の
StateFlow 受信 Aは初期値です。そのあとAを代入しても、今の値と同じなので StateFlow の購読者には届きません。2回目のBも同様です。SharedFlow は同じ値でも毎回届けています。 - StateFlow に X・Y・Z を続けて代入すると、届いたのは最後の Z だけです。StateFlow は「最新の値」を伝えるもので、途中の値をすべて届ける用途には向きません。1件ずつ確実に伝えたいイベントには SharedFlow を使います。
公式の API ドキュメントは、StateFlow の値は Any.equals の比較でまとめられ distinctUntilChanged と似た動きをすること、StateFlow は「replay = 1、あふれたら最も前の値を捨てる設定の SharedFlow に初期値を入れ、distinctUntilChanged を付けたもの」と同じ動きをすることを説明しています。data class を値にする場合は、中身が同じなら流れないので、copy で内容を変えた新しいインスタンスを代入します。
演算子の一覧表
flowOf(3, 1, 1, 2, 2, 5) を入力にして実行した結果です。表の右列は実際の出力をそのまま載せています。
途中でつなぐ演算子(Flow を返す)
| 演算子 | 働き | 書き方 | 結果 |
|---|---|---|---|
map |
値を変換する | map { it * 2 } |
[6, 2, 2, 4, 4, 10] |
filter |
条件に合う値だけ通す | filter { it > 1 } |
[3, 2, 2, 5] |
mapNotNull |
変換して null を除く | mapNotNull { if (it > 2) "v$it" else null } |
[v3, v5] |
transform |
1つの値から0個以上を emit | transform { if (it > 2) { emit(it); emit(-it) } } |
[3, -3, 5, -5] |
take |
先頭から n 個で止める | take(2) |
[3, 1] |
takeWhile |
条件を満たす間だけ通す | takeWhile { it != 2 } |
[3, 1, 1] |
drop |
先頭の n 個を捨てる | drop(2) |
[1, 2, 2, 5] |
dropWhile |
条件を満たす間は捨てる | dropWhile { it != 2 } |
[2, 2, 5] |
distinctUntilChanged |
直前と同じ値を除く | distinctUntilChanged() |
[3, 1, 2, 5] |
withIndex |
番号を付ける | take(2).withIndex() |
[IndexedValue(index=0, value=3), IndexedValue(index=1, value=1)] |
scan |
途中経過つきの畳み込み | scan(0) { a, v -> a + v } |
[0, 3, 4, 5, 7, 9, 14] |
runningReduce |
初期値なしの途中経過 | runningReduce { a, v -> a + v } |
[3, 4, 5, 7, 9, 14] |
onEach |
値を変えずに処理をはさむ | onEach { println(it) } |
値はそのまま |
onStart |
流れ始める前に実行 | take(1).onStart { emit(0) } |
[0, 3] |
onCompletion |
終わったときに実行 | take(1).onCompletion { emit(99) } |
[3, 99] |
onEmpty |
1つも流れなかったとき | emptyFlow<Int>().onEmpty { emit(-1) } |
[-1] |
zip |
2つを順番に組にする | zip(flowOf("a", "b")) { n, s -> "$n$s" } |
[3a, 1b] |
combine |
それぞれの最新値を組にする | flowOf(1, 2).combine(flowOf("a")) { n, s -> "$n$s" } |
[1a, 2a] |
merge |
複数の Flow を1本にまとめる | merge(flowOf(1, 2), flowOf(10)) |
[1, 2, 10] |
flatMapConcat |
値ごとに Flow を作り順につなぐ | flowOf(1, 2).flatMapConcat { flowOf(it, it * 10) } |
[1, 10, 2, 20] |
flowOn |
上流を動かすスレッドを変える | flowOn(Dispatchers.Default) |
次の節で確認 |
catch |
上流の例外を受ける | catch { e -> emit(-1) } |
次の節で確認 |
retry |
例外が出たら最初からやり直す | retry(2) |
次の節で確認 |
終端演算子(suspend 関数。呼ぶと Flow が動く)
| 演算子 | 働き | 結果 | 該当する値がないとき |
|---|---|---|---|
collect { } |
1つずつ処理する | 戻り値なし | 何もせず終わる |
collect() |
流すだけ(処理は onEach に書く) |
戻り値なし | 何もせず終わる |
toList |
List にする | [3, 1, 1, 2, 2, 5] |
空の List |
toSet |
Set にする | [3, 1, 2, 5] |
空の Set |
first |
最初の1個で止める | 3 |
NoSuchElementException: Expected at least one element |
first { } |
条件に合う最初の1個 | first { it < 3 } は 1 |
同上の例外 |
firstOrNull |
最初の1個か null | 空の Flow で null |
null |
last / lastOrNull |
最後の1個 | 5 / 空の Flow で null |
lastOrNull は null |
single |
1個だけ流れる Flow の値 | flowOf(7).single() は 7 |
複数あると IllegalArgumentException: Flow has more than one element |
singleOrNull |
1個だけなら値、それ以外は null | 6個の Flow で null |
null |
count |
個数 | 6、count { it > 1 } は 4 |
0 |
reduce |
畳み込み(初期値なし) | 14 |
NoSuchElementException: Empty flow can't be reduced |
fold |
畳み込み(初期値あり) | fold(100) で 114 |
初期値 |
launchIn(scope) |
別のコルーチンで collect を始める | Job を返す | 後の節で確認 |
時間に関わる演算子
30 ミリ秒おきに 1〜5 を流す Flow を、処理に時間がかかる受け取り側で試した結果です。
| 演算子 | 働き | 結果 |
|---|---|---|
collectLatest { } |
新しい値が来たら処理中のブロックを取り消す(50 ミリ秒かかる処理) | [5] |
conflate() |
受け取り側が遅いとき途中の値を飛ばす(100 ミリ秒かかる処理) | [1, 4, 5] |
buffer() |
上流を待たせずにためておく | [1, 2, 3, 4, 5] |
debounce(50) |
50 ミリ秒のあいだ次が来なかった値だけ通す(1 → 10ms → 2 → 100ms → 3 の入力) | [2, 3] |
sample(50) |
50 ミリ秒ごとに最新値を1つ取り出す | [2, 4, 5] |
debounce と sample は 1.8.0 では @OptIn(FlowPreview::class)、flatMapConcat は @OptIn(ExperimentalCoroutinesApi::class) を付けて使いました。上の3つの表を作ったコードの全文です。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun src() = flowOf(3, 1, 1, 2, 2, 5)
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
fun main() = runBlocking {
println("map " + src().map { it * 2 }.toList())
println("filter " + src().filter { it > 1 }.toList())
println("mapNotNull " + src().mapNotNull { if (it > 2) "v$it" else null }.toList())
println("transform " + src().transform { if (it > 2) { emit(it); emit(-it) } }.toList())
println("take(2) " + src().take(2).toList())
println("takeWhile " + src().takeWhile { it != 2 }.toList())
println("drop(2) " + src().drop(2).toList())
println("dropWhile " + src().dropWhile { it != 2 }.toList())
println("distinctUntilChanged " + src().distinctUntilChanged().toList())
println("withIndex " + src().take(2).withIndex().toList())
println("scan " + src().scan(0) { a, v -> a + v }.toList())
println("runningReduce " + src().runningReduce { a, v -> a + v }.toList())
println("onStart " + src().take(1).onStart { emit(0) }.toList())
println("onCompletion " + src().take(1).onCompletion { emit(99) }.toList())
println("onEmpty " + emptyFlow<Int>().onEmpty { emit(-1) }.toList())
println("zip " + src().zip(flowOf("a", "b")) { n, s -> "$n$s" }.toList())
println("combine " + flowOf(1, 2).combine(flowOf("a")) { n, s -> "$n$s" }.toList())
println("merge " + merge(flowOf(1, 2), flowOf(10)).toList())
println("flatMapConcat " + flowOf(1, 2).flatMapConcat { flowOf(it, it * 10) }.toList())
println("--- 終端演算子 ---")
println("toList " + src().toList())
println("toSet " + src().toSet())
println("first " + src().first())
println("first { } " + src().first { it < 3 })
println("firstOrNull " + emptyFlow<Int>().firstOrNull())
println("last " + src().last())
println("lastOrNull " + emptyFlow<Int>().lastOrNull())
println("single " + flowOf(7).single())
println("singleOrNull " + src().singleOrNull())
println("count " + src().count())
println("count { } " + src().count { it > 1 })
println("reduce " + src().reduce { a, v -> a + v })
println("fold " + src().fold(100) { a, v -> a + v })
println("--- 空の Flow で例外になるもの ---")
for ((name, f) in listOf<Pair<String, suspend () -> Any?>>(
"first" to { emptyFlow<Int>().first() },
"single(複数)" to { src().single() },
"reduce" to { emptyFlow<Int>().reduce { a, v -> a + v } },
)) {
try { f() } catch (e: Exception) { println("$name -> ${e::class.simpleName}: ${e.message}") }
}
println("--- 時間に関わる演算子 ---")
val slow = flow { for (i in 1..5) { emit(i); delay(30) } }
val got = mutableListOf<Int>()
slow.collectLatest { delay(50); got += it }
println("collectLatest $got")
println("conflate " + slow.conflate().onEach { delay(100) }.toList())
println("buffer " + slow.buffer().onEach { delay(100) }.toList())
println("debounce(50) " + flow { emit(1); delay(10); emit(2); delay(100); emit(3) }.debounce(50).toList())
println("sample(50) " + slow.sample(50).toList())
}
flowOn:上流を動かすスレッドを切り替える
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun th() = Thread.currentThread().name
fun main() = runBlocking {
flow {
println("emit : ${th()}")
emit(1)
}
.map { println("map 上 : ${th()}"); it * 2 }
.flowOn(Dispatchers.Default)
.map { println("map 下 : ${th()}"); it + 1 }
.collect { println("collect: ${th()} 値=$it") }
}
emit : DefaultDispatcher-worker-1
map 上 : DefaultDispatcher-worker-1
map 下 : main
collect: main 値=3
flowOn が切り替えるのは、自分より上に書いた部分(上流)だけです。 flowOn より下の map と collect は、collect を呼んだ側のスレッド(ここでは main)のままです。重い読み込みを別スレッドに移し、結果の表示は呼び出し側で行う、という分担が1行で書けます。
flow { } の中で withContext を使って emit すると、次の例外になります。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flow {
withContext(Dispatchers.Default) {
emit(1)
}
}.collect { println(it) }
}
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
Flow was collected in [BlockingCoroutine{Active}@30bad64, BlockingEventLoop@41fda58c],
but emission happened in [DispatchedCoroutine{Active}@47cdf8b, Dispatchers.Default].
Please refer to 'flow' documentation or use 'flowOn' instead
at kotlinx.coroutines.flow.internal.SafeCollector_commonKt.checkContext(SafeCollector.common.kt:84)
…(省略)…
メッセージの最後にあるとおり、スレッドを変えたいときは withContext ではなく flowOn を使います。公式ドキュメントでも、flow() ビルダーは動いているコンテキストと同じコンテキストから emit する必要があり、別のコンテキストで動かしたいときは flowOn か channelFlow() を使うと案内されています。
catch・onCompletion・retry:例外の扱い方
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun source(): Flow<Int> = flow {
emit(1)
emit(2)
throw IllegalStateException("読み込みが止まりました")
}
fun main() = runBlocking {
println("1) catch で受けて代わりの値を流す")
source()
.catch { e -> println(" catch: ${e.message}"); emit(-1) }
.onCompletion { cause -> println(" onCompletion cause=$cause") }
.collect { println(" 受信 $it") }
println("2) catch より下流の例外は catch に届かない")
try {
source()
.catch { e -> println(" catch: ${e.message}") }
.collect { v ->
check(v < 2) { "collect の中で検査に引っかかった: $v" }
println(" 受信 $v")
}
} catch (e: IllegalStateException) {
println(" try-catch: ${e.message}")
}
println("3) onEach に処理を移すと catch で受けられる")
source()
.onEach { v ->
check(v < 2) { "onEach の中で検査に引っかかった: $v" }
println(" 受信 $v")
}
.catch { e -> println(" catch: ${e.message}") }
.collect()
println("4) retry でやり直す")
var attempt = 0
flow {
attempt++
println(" $attempt 回目")
if (attempt < 3) throw IllegalStateException("まだ準備中")
emit("OK")
}
.retry(2)
.collect { println(" 受信 $it") }
}
1) catch で受けて代わりの値を流す
受信 1
受信 2
catch: 読み込みが止まりました
受信 -1
onCompletion cause=null
2) catch より下流の例外は catch に届かない
受信 1
try-catch: collect の中で検査に引っかかった: 2
3) onEach に処理を移すと catch で受けられる
受信 1
catch: onEach の中で検査に引っかかった: 2
4) retry でやり直す
1 回目
2 回目
3 回目
受信 OK
| やりたいこと | 書き方 | 出力で確認できたこと |
|---|---|---|
| 上流の例外を受けて代わりの値を流す | .catch { emit(-1) } |
例外のあと -1 が届き、正常に完了(cause=null) |
| collect の中の例外も受けたい | 処理を onEach に移し、.catch { } のあと .collect() |
3) では catch に届いた |
| collect の中の例外を呼び出し側で受ける | try { ... collect { } } catch |
2) では try-catch に届いた |
| 何回かやり直す | .retry(回数) |
2回やり直して3回目に成功 |
| 終わったときに必ず実行 | .onCompletion { cause -> } |
正常終了なら cause は null |
catch が受けるのは、自分より上流で起きた例外だけです。 2) のように collect のラムダで起きた例外は catch を通りません。公式ドキュメントも、collect に渡したラムダの例外は try-catch で collect を囲んで扱うと説明しています。
collect の次の行が実行されないとき:launchIn での書き分け
StateFlow と SharedFlow は完了しないので、collect は cancel されるまで戻ってきません。collect の下に書いた行は実行されないままになります。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val state = MutableStateFlow(0)
val job = launch {
println("collect を始める")
state.collect { println(" 受信 $it") }
println("この行は実行されない")
}
delay(100)
state.value = 1
delay(100)
println("job.isActive = ${job.isActive} isCompleted = ${job.isCompleted}")
job.cancel()
job.join()
println("cancel 後 isCompleted = ${job.isCompleted}")
}
collect を始める
受信 0
受信 1
job.isActive = true isCompleted = false
cancel 後 isCompleted = true
この行は実行されない は表示されず、collect を呼んだコルーチンは cancel するまで動いたままです。公式の API ドキュメントにも、StateFlow は完了せず、collect の呼び出しは正常には終わらないと書かれています。
この性質から、1つの launch に collect を2つ並べると、2つ目が始まりません。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val count = MutableStateFlow(0)
val message = MutableSharedFlow<String>()
println("--- 1つの launch に collect を2つ並べる ---")
val bad = launch {
count.collect { println(" count 受信 $it") }
message.collect { println(" message 受信 $it") } // ここまで進まない
}
delay(50)
count.value = 1
message.emit("こんにちは")
delay(50)
bad.cancel()
println("--- launchIn で1本ずつ起動する ---")
val scope = CoroutineScope(coroutineContext + Job())
count.onEach { println(" count 受信 $it") }.launchIn(scope)
message.onEach { println(" message 受信 $it") }.launchIn(scope)
println("launchIn の次の行はすぐ実行される")
delay(50)
count.value = 2
message.emit("こんにちは")
delay(50)
scope.cancel()
}
--- 1つの launch に collect を2つ並べる ---
count 受信 0
count 受信 1
--- launchIn で1本ずつ起動する ---
launchIn の次の行はすぐ実行される
count 受信 1
count 受信 2
message 受信 こんにちは
前半では message 受信 が一度も出ていません。後半のように onEach { 処理 }.launchIn(scope) と書くと、collect が別のコルーチンで始まり、次の行へすぐ進みます。launchIn(scope) は scope.launch { flow.collect() } を短く書いたもので、戻り値の Job や scope の cancel で止められます。
| 書き方 | 次の行に進むタイミング | 向いている場面 |
|---|---|---|
flow.collect { } |
Flow が完了したあと(ホットフローでは cancel まで進まない) | 完了する Flow を最後まで処理してから先へ進みたい |
launch { flow.collect { } } |
すぐ | 1本ずつ別のコルーチンで受け取る |
flow.onEach { }.launchIn(scope) |
すぐ | 上と同じ内容を、つなげた形で書きたい |
flow.first() / flow.take(n).collect { } |
必要な個数を受け取ったあと | ホットフローから決まった個数だけ受け取る |
stateIn と shareIn:コールドフローをホットに変える
shareIn は Flow を SharedFlow に、stateIn は StateFlow に変えます。どちらも、上流を動かす CoroutineScope と、いつ動かし始めるかを決める started を渡します。started の3種類を、同じ手順(250 ミリ秒後に購読、さらに 250 ミリ秒後に購読をやめる)で比べます。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
val t0 = System.currentTimeMillis()
fun log(msg: String) = println("[%4d ms] %s".format(System.currentTimeMillis() - t0, msg))
fun ticker(name: String): Flow<Int> = flow {
log("$name: 上流 開始")
try {
var i = 0
while (true) {
i++
log("$name: 上流 emit $i")
emit(i)
delay(100)
}
} finally {
log("$name: 上流 停止")
}
}
suspend fun run(name: String, started: SharingStarted) = coroutineScope {
val scope = CoroutineScope(coroutineContext + Job())
val t = System.currentTimeMillis()
log("=== $name ===")
val shared = ticker(name).shareIn(scope, started, replay = 1)
delay(250)
log("$name: 購読者が collect 開始")
val job = launch { shared.collect { log("$name: 購読者 受信 $it") } }
delay(250)
log("$name: 購読者が cancel")
job.cancel()
delay(250)
log("$name: scope を cancel")
scope.cancel()
delay(50)
}
fun main() = runBlocking {
run("Eagerly", SharingStarted.Eagerly)
run("Lazily", SharingStarted.Lazily)
run("WhileSubscribed", SharingStarted.WhileSubscribed())
}
[ 129 ms] === Eagerly ===
[ 191 ms] Eagerly: 上流 開始
[ 192 ms] Eagerly: 上流 emit 1
[ 300 ms] Eagerly: 上流 emit 2
[ 402 ms] Eagerly: 上流 emit 3
[ 433 ms] Eagerly: 購読者が collect 開始
[ 478 ms] Eagerly: 購読者 受信 3
[ 503 ms] Eagerly: 上流 emit 4
[ 503 ms] Eagerly: 購読者 受信 4
[ 604 ms] Eagerly: 上流 emit 5
[ 605 ms] Eagerly: 購読者 受信 5
[ 704 ms] Eagerly: 上流 emit 6
[ 705 ms] Eagerly: 購読者 受信 6
[ 709 ms] Eagerly: 購読者が cancel
[ 805 ms] Eagerly: 上流 emit 7
[ 908 ms] Eagerly: 上流 emit 8
[ 963 ms] Eagerly: scope を cancel
[ 964 ms] Eagerly: 上流 停止
[1014 ms] === Lazily ===
[1278 ms] Lazily: 購読者が collect 開始
[1285 ms] Lazily: 上流 開始
[1285 ms] Lazily: 上流 emit 1
[1285 ms] Lazily: 購読者 受信 1
[1385 ms] Lazily: 上流 emit 2
[1386 ms] Lazily: 購読者 受信 2
[1492 ms] Lazily: 上流 emit 3
[1501 ms] Lazily: 購読者 受信 3
[1529 ms] Lazily: 購読者が cancel
[1601 ms] Lazily: 上流 emit 4
[1702 ms] Lazily: 上流 emit 5
[1780 ms] Lazily: scope を cancel
[1780 ms] Lazily: 上流 停止
[1832 ms] === WhileSubscribed ===
[2156 ms] WhileSubscribed: 購読者が collect 開始
[2159 ms] WhileSubscribed: 上流 開始
[2162 ms] WhileSubscribed: 上流 emit 1
[2162 ms] WhileSubscribed: 購読者 受信 1
[2262 ms] WhileSubscribed: 上流 emit 2
[2263 ms] WhileSubscribed: 購読者 受信 2
[2363 ms] WhileSubscribed: 上流 emit 3
[2364 ms] WhileSubscribed: 購読者 受信 3
[2408 ms] WhileSubscribed: 購読者が cancel
[2409 ms] WhileSubscribed: 上流 停止
[2659 ms] WhileSubscribed: scope を cancel
started |
上流が動き始めるとき | 購読者がいなくなったあと | 向いている場面 |
|---|---|---|---|
SharingStarted.Eagerly |
shareIn を呼んだ直後(購読者がいなくても) |
動き続ける(scope の cancel で停止) | 起動時から値を用意しておきたい |
SharingStarted.Lazily |
最初の購読者が来たとき | 動き続ける(scope の cancel で停止) | 使われるまで動かさず、一度始めたら続けたい |
SharingStarted.WhileSubscribed() |
購読者が来たとき | すぐ止まる(次の購読でまた最初から) | 見ている人がいる間だけ動かしたい |
Eagerly では購読者が来る前に 1・2・3 が流れ、購読者は replay = 1 の分の 3 から受け取りました。WhileSubscribed では購読をやめた 1 ミリ秒後に 上流 停止 が出ています。公式の API ドキュメントによると、WhileSubscribed には stopTimeoutMillis(最後の購読者がいなくなってから上流を止めるまでの待ち時間)と replayExpirationMillis(上流を止めてから replay 用の値を消すまでの時間)を渡せます。
stateIn は初期値を渡す
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val scope = CoroutineScope(coroutineContext + Job())
val source = flow {
println("上流 開始")
for (i in 1..3) { delay(100); emit(i) }
}
val state: StateFlow<Int> = source.stateIn(scope, SharingStarted.Eagerly, initialValue = 0)
println("直後の value = ${state.value}")
delay(250)
println("250ms 後の value = ${state.value}")
val first: StateFlow<Int> = source.stateIn(scope) // 最初の値が届くまで待つ suspend 版
println("suspend 版の value = ${first.value}")
scope.cancel()
}
直後の value = 0
上流 開始
250ms 後の value = 2
上流 開始
suspend 版の value = 1
stateIn(scope, started, initialValue) は、上流の最初の値が届くまで initialValue を持ちます。引数が scope だけの stateIn(scope) は suspend 関数で、最初の値が届くまで待ってから StateFlow を返すので、初期値を渡さずに済みます。
つまずきやすい点のチェックリスト
| 症状 | 原因 | 直し方 |
|---|---|---|
| Flow を作ったのに何も起きない | 終端演算子を呼んでいない | collect・toList・launchIn などを付ける |
| collect の次の行に進まない | StateFlow / SharedFlow は完了しない | launch で囲むか launchIn(scope) を使う |
| 2つ目の collect が動かない | 1つの launch に collect を並べている | collect ごとに launch / launchIn を分ける |
| SharedFlow の値を受け取れない | emit のあとに購読した(replay が 0) | 先に購読を始めるか replay を指定する |
| StateFlow に代入したのに届かない | 今の値と equals で同じ |
内容を変えた新しいインスタンスを代入する |
| StateFlow で途中の値が抜ける | 最新値だけを保つ仕組み | すべて届けたい値は SharedFlow で流す |
Flow invariant is violated |
flow の中で withContext して emit |
flowOn に置き換える |
| catch に例外が届かない | collect のラムダで起きた例外 | 処理を onEach に移すか try-catch で囲む |
| 同じ処理が購読者の数だけ走る | コールドフローを複数箇所で collect | shareIn / stateIn で共有する |
よくある質問
Q. StateFlow と SharedFlow はどちらを使えばよいですか? A. 「今の値」をいつでも読めて、あとから来た購読者にも最新値を渡したいなら StateFlow です。同じ内容でも毎回届けたい通知や、1件ずつ確実に処理したい出来事には SharedFlow が合います。
Q. Flow とコルーチンの suspend 関数はどう使い分けますか?
A. 結果が1つなら suspend 関数、時間をおいて複数の値が届くなら Flow です。Flow から値を1つだけ取り出すときは first() が使えます。
Q. MutableStateFlow と StateFlow の違いは何ですか?
A. MutableStateFlow は value に代入できる型、StateFlow は読み取り専用の型です。クラスの中で MutableStateFlow を private に持ち、外には asStateFlow() で StateFlow として公開すると、値を書き換える場所を1か所にまとめられます。SharedFlow も asSharedFlow() で同じ形にできます。
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
class Counter {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow() // 外には読み取り専用で公開
private val _events = MutableSharedFlow<String>(replay = 1)
val events: SharedFlow<String> = _events.asSharedFlow()
fun increment() {
_count.update { it + 1 } // 今の値をもとに更新
println("tryEmit = " + _events.tryEmit("count が ${_count.value} になりました"))
}
}
fun main() = runBlocking {
val c = Counter()
c.increment()
c.increment()
println("count.value = ${c.count.value}")
println("events.replayCache = ${c.events.replayCache}")
println("first = ${c.events.first()}")
}
tryEmit = true
tryEmit = true
count.value = 2
events.replayCache = [count が 2 になりました]
first = count が 2 になりました
update { } は今の値をもとに次の値を決める書き方、tryEmit は suspend せずに値を入れて、入ったかどうかを Boolean で返す関数です。
Q. collect を止めるにはどうしますか?
A. collect を呼んだコルーチンの Job を cancel() するか、launchIn に渡した scope を cancel します。決まった個数で終えたいときは take(n) や first() を使うと、受け取った時点で自然に終わります。
まとめ
flow { }はコールドフロー。collect のたびにブロックが最初から実行され、あとから collect しても全部受け取れる- StateFlow / SharedFlow はホットフロー。あとから来た購読者に届くのは replay の分だけ(StateFlow は最新の1個)
- StateFlow は初期値が必須で、同じ値を間引き、
valueで今の値を読める。SharedFlow は同じ値も毎回届ける - 演算子は「Flow を返すもの」と「終端演算子」に分かれる。終端演算子を呼んで初めて Flow が動く
flowOnは上流のスレッドだけを変え、catchは上流の例外だけを受ける- ホットフローの collect は終わらない。次の行へ進みたいときは
launchで囲むかonEach { }.launchIn(scope)と書く stateIn/shareInのstartedは、Eagerly(すぐ開始)・Lazily(最初の購読で開始)・WhileSubscribed(購読者がいる間だけ)から選ぶ
参考・出典
- Kotlin 公式ドキュメント「Asynchronous flow」: https://kotlinlang.org/docs/coroutines-flow.html
- kotlinx.coroutines API「StateFlow」: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/
- kotlinx.coroutines API「SharingStarted」: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/
- kotlinx.coroutines README(依存の書き方): https://github.com/Kotlin/kotlinx.coroutines/blob/master/README.md
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Flowの使い方 StateFlowとSharedFlowの違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。