KotlinのFlowについて

採用はこちら

KotlinのFlowは、コルーチンベースで非同期のデータの流れを扱うための仕組みです。

普通の関数が値を1回返して終わるのに対して、Flowは時間の経過に応じて複数の値を順番に流すことができます。

そのため、単発の処理というより、継続的に変化するデータを扱いたいときに向いています。

たとえば、次のようなケースです。

  • 検索欄の入力内容が変わるたびに結果を更新したい
  • データベースの変更を監視したい
  • APIから一定間隔で最新情報を取りたい
  • UIの状態を継続的に更新したい

こうした「一度だけ結果を返す」のではなく、「変化する値を流し続ける」場面でFlowが役立ちます。

目次

Flowの基本的な考え方

Flowを理解するうえで、まず大事なのは次の点です。

  • Flowは非同期に複数の値を扱える
  • flow {} で作る通常のFlowはcold
  • StateFlowSharedFlowhot
  • collect() が呼ばれて初めて処理が動くことが多い

ここで特に重要なのが、coldhot の違いです。

flow {} で作るFlowは通常 cold

Kotlinで最も基本的なFlowは、flow {} を使って作ります。

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking

fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(1000)
        emit(i)
    }
}

fun main() = runBlocking {
    simpleFlow().collect { value ->
        println(value)
    }
}

このコードでは、1秒ごとに 1, 2, 3 が順番に流れます。

  • emit() は値を流す
  • collect() は値を受け取る

という役割です。

そして flow {} で作ったFlowは、collectされるまで動きません

さらに、collectされるたびに最初から処理が実行されます

つまり、同じFlowに対して2回 collect() すると、上流の処理も2回実行されます。

val f = flow {
    println("start")
    emit(1)
    emit(2)
}

runBlocking {
    f.collect { println(it) }
    f.collect { println(it) }
}

この場合、start は2回表示されます。

ただし、ここで注意したいのは、Flowという型そのものが必ずcoldというわけではないことです。

flow {} で作る一般的なFlowはcoldですが、StateFlowSharedFlow はhot flowです。

suspend fun と Flow の違い

この違いはとても大切です。

suspend fun

非同期に1つの結果を返す

suspend fun fetchUser(): User

Flow

非同期に0個以上の値を順番に流す

fun observeUsers(): Flow<User>

かなりシンプルに言えば、

  • 1回だけ値を取ればいいなら suspend fun
  • 継続的な変化を扱いたいなら Flow

という考え方で大丈夫です。

Flowは「演算子をつないで使う」

Flowの大きな特徴は、演算子をつないでデータを加工できることです。

たとえば、mapfilter などを使うと、値を変換したり絞り込んだりできます。

flowOf(1, 2, 3, 4)
    .filter { it % 2 == 0 }
    .map { it * 10 }
    .collect { println(it) }

この場合は、偶数だけを取り出して10倍してから受け取ります。

ここで知っておきたいのが、Flowの演算子には大きく2種類あるということです。

中間演算子

新しいFlowを返すだけで、その場ではまだ実行しません。

  • map
  • filter
  • onEach
  • catch

終端演算子

Flowの収集を開始します。

  • collect
  • first
  • single
  • toList

つまり、Flowでは「加工のパイプラインを作って、最後にcollectして動かす」という流れになります。

catch は「上流の例外」を扱う

catch は便利ですが、ここは誤解しやすいところです。

catch は、自分より上流で発生した例外を処理します。

逆に、下流で起きた例外は基本的にcatchできません

flow { emitData() }
    .map { computeOne(it) }
    .catch { e -> println("error: $e") }
    .map { computeTwo(it) }
    .collect { process(it) }

この場合、emitData()computeOne() の例外は catch の対象になります。

しかし、computeTwo()process() で起きた例外は、通常この catch では拾えません。

そのため、catch は単なる「何でも拾う例外処理」ではなく、上流に対する例外処理だと理解しておくのが大事です。

flowOn は上流の実行コンテキストを変える

Flowでは、どのコンテキストで処理を動かすかも重要です。

そのために使うのが flowOn です。

flow
    .map { heavyWork(it) }
    .flowOn(Dispatchers.IO)
    .collect { println(it) }

ただし、flowOn はFlow全体に効くわけではありません。

flowOn より上流の処理だけに影響します。

この点を理解していないと、「IOで動かしたつもりだったのに違った」というズレが起こりやすいです。

つまり flowOn は、Flow全体のスレッド切り替えではなく、上流側のコンテキスト変更と考えるのが正確です。

Flowは基本的に順次実行される

Flowは非同期処理を扱いますが、だからといって何でも自動的に並列になるわけではありません。

基本的には順番に処理されます

必要な場合だけ、bufferconflateflatMapMerge などで並行性や処理の調整を導入します。

この感覚はかなり大事です。

Flowは「勝手に並列になる仕組み」ではなく、基本は順次、必要なら明示的に並行性を加える仕組みです。

collectLatest は「最新を優先する」

collectLatest は実務でとてもよく使います。

これは、新しい値が来たときに、前の処理をキャンセルして最新の値の処理に切り替えるためのものです。

queryFlow.collectLatest { query ->
    performSearch(query)
}

たとえば検索欄で文字入力が続いているとき、古い検索処理を最後までやる必要はないことが多いです。

そういうときに collectLatest が向いています。

普通の collect は全部順番に処理しますが、collectLatest古い処理より最新の値を優先するという違いがあります。

flowchannelFlow の違い

通常の flow {} は、順番に値を流すシンプルなFlow を作るのに向いています。

一方、複数のコルーチンから同時に値を送りたい場合や、コールバックAPIをFlowに変換したい場合は、channelFlow を検討します。

ざっくり整理するとこうです。

  • 単純に順番に流したい → flow
  • 複数の非同期発生源をまとめたい → channelFlow

普段はまず flow を使い、必要があるときだけ channelFlow を使うと考えると分かりやすいです。

StateFlow とは何か

StateFlow は、現在の状態を保持するためのhot flowです。

特徴は次の通りです。

  • 常に現在値を持つ
  • 初期値が必要
  • 新しく購読した側も最新値を受け取れる
  • UI状態の管理に向いている
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count

これは「今の状態」を表すのにとても向いています。

たとえば、

  • ローディング中かどうか
  • 一覧データの現在値
  • フォーム入力の現在状態
  • 画面に表示すべきUI状態

などです。

StateFlow が状態向きな理由

StateFlow は、常に「今の値」を持っています。

そのため、画面が再購読してもすぐに現在状態を取得できます。

一方で、これは逆に言うと、同じ内容を何度も単発イベントとして飛ばす用途にはあまり向かないことも意味します。

たとえば、「保存失敗のToastを毎回出したい」のようなケースでは、状態よりイベントとして扱った方が自然です。

このあたりが、StateFlowSharedFlow の使い分けにつながります。

SharedFlow とは何か

SharedFlow は、複数のcollectorに値を共有して配信するhot flowです。

特徴は次の通りです。

  • hot flow
  • 複数のcollectorに同じ値を配信できる
  • replay などの設定ができる
  • 共有イベントや共有ストリームに向いている
private val _events = MutableSharedFlow<String>()
val events = _events

SharedFlow は、しばしば単発イベントに使われます。

たとえば、

  • Toast表示
  • Snackbar表示
  • 画面遷移
  • ダイアログ表示指示

などです。

ただし、ここで大事なのは、SharedFlow はイベント専用と決まっているわけではないということです。

実務ではイベント用途に使われることが多いだけで、仕組みとしてはもっと広く使えます。

StateFlowSharedFlow の使い分け

実務では、次のように考えると分かりやすいです。

StateFlow

向いているもの

  • 現在の画面状態
  • 入力フォームの状態
  • 一覧データの最新状態
  • ローディング状態

SharedFlow

向いているもの

  • 一回限りのイベント
  • 通知
  • 画面遷移
  • SnackbarやToast表示

かなり雑に言えば、

  • 状態はStateFlow
  • イベントはSharedFlow

です。

ただしこれは仕様上の絶対ルールではなく、設計としてそうすると分かりやすいという実務的な整理です。

cold flow を共有したいときは shareInstateIn

通常のcold flowは、collect のたびに上流が再実行されます。

それが困る場合があります。

たとえば、

  • APIアクセスを何度も走らせたくない
  • DB監視の上流を複数画面で共有したい
  • 同じデータソースを複数のcollectorで使いたい

といったケースです。

そういうときに使うのが shareInstateIn です。

  • stateIn はFlowを StateFlow として扱いやすくする
  • shareIn は上流を共有して SharedFlow 的に使えるようにする

つまり、cold flowをそのまま個別に動かすのではなく、共有可能なhot flowに変換するための仕組みです。

Flowを使うときの実務的な理解

ここまでを実務向けにまとめると、次の整理が一番使いやすいです。

suspend fun

  • 単発処理向け
  • 1回だけ結果が欲しいときに使う

flow {} で作るFlow

  • 継続的な値の流れを表す
  • 通常はcold
  • collect ごとに再実行される

StateFlow

  • 現在値を持つ
  • hot
  • UI状態管理に向いている

SharedFlow

  • 値を複数に共有できる
  • hot
  • 単発イベントや共有通知に向いている

この4つを整理しておくと、Flowまわりの理解がかなり安定します。

最後に覚えておきたい要点

Flowを最初に学ぶときは、全部を一気に覚える必要はありません。

まずは次のポイントだけ押さえると十分です。

  • Flowは非同期の値の流れ
  • flow {} は通常 cold
  • collect() で収集開始
  • StateFlow状態
  • SharedFlow共有イベントに向く
  • catch上流の例外
  • flowOn上流のコンテキスト変更
  • collectLatest最新値を優先

この整理が頭に入ると、Flowはかなり理解しやすくなります。

以上、KotlinのFlowについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次