はじめに
Kotlin の Flow は、非同期ストリームを安全に扱える強力な仕組みです。
しかし、通信エラー・一時的な失敗・画面再生成 などの場面では、
「再試行(Retry)」と「再発行(Replay)」を正しく設計しないと
UI が止まったり、二重リクエストが発生したりします。
この記事では、それぞれの動作原理と実践的な設計パターンを解説します。
エラーリトライ(Retry)
概要
Flow がエラーを発生したとき、自動的に再試行する仕組み。
ネットワーク不安定や一時的なサーバエラーに有効です。
retry オペレーター
val result = flow {
emit(api.fetchData()) // ネットワーク通信
}.retry(3) { e ->
e is IOException // IOException の場合のみ再試行
}.catch { e ->
emit("Fallback: ${e.message}")
}
最大 3 回まで再試行。
条件関数の戻り値が true なら再試行されます。
動作イメージ
retryWhen オペレーター
条件をより細かく制御できます。
flow {
emit(api.fetchData())
}.retryWhen { cause, attempt ->
if (cause is IOException && attempt < 3) {
delay(1000L * attempt) // 指数的バックオフ
true
} else {
false
}
}.catch {
emit("Failed after retries")
}
例:通信失敗時に 1秒 → 2秒 → 3秒 の間隔で再試行。
Tips
| 状況 | 対応策 |
|---|---|
| API 一時エラー |
retryWhen で指数バックオフ |
| 例外をログ出力したい |
catch 内で Log.e()
|
| UI に再試行ボタンを出したい | ViewModel 側で emit(RetryState.Failed) を通知 |
リプレイ(Replay)
概要
過去に emit された値を再送(再発行)する仕組み。
主に SharedFlow/StateFlow で使われます。
StateFlow のリプレイ
StateFlow は常に 最新の値を1つ保持 しています。
新しいコレクタが現れると、その「最新値」が即座に再送されます。
val counter = MutableStateFlow(0)
fun main() = runBlocking {
launch {
repeat(3) {
delay(100)
counter.value = it
}
}
delay(250)
counter.collect { println("Collector: $it") }
}
出力:
Collector: 2
collect した瞬間に「最新値」が再発行されている。
これが 1件リプレイ の仕組みです。
SharedFlow のリプレイ
MutableSharedFlow では、任意の件数のリプレイが可能です。
val events = MutableSharedFlow<String>(replay = 2)
fun main() = runBlocking {
launch {
events.emit("A")
events.emit("B")
events.emit("C")
}
delay(100)
events.collect { println("Collector: $it") }
}
出力:
Collector: B
Collector: C
過去2件(B, C)が新規購読者に再送される。
replayCache で過去値を参照
println(events.replayCache) // 現在のリプレイ値リスト
Flow を Hot 化してリプレイ可能にする
shareIn() を使えば、Cold Flow を ホット + リプレイ可能 に変換できます。
val hotFlow = coldFlow.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
replay = 1
)
replay = 1 により、最新値を1件保持。
画面回転や再購読時にも値をすぐに受け取れる。
Retry × Replay 組み合わせ設計例
ViewModel 層での実戦構成
val uiState = repository.loadUser()
.retryWhen { e, attempt ->
e is IOException && attempt < 3
}
.catch { emit(UserState.Error(it.message)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = UserState.Loading
)
特徴:
- Cold Flow(データ取得)を
stateIn()で Hot 化し、最新状態をリプレイ。 - 通信失敗は
retryWhenで最大3回まで自動再試行。
Retry / Replay の比較表
| 機能 | 対象 | タイミング | 主な用途 |
|---|---|---|---|
| Retry | Flow のエラー | エラー発生時 | ネットワーク再試行 |
| Replay | Flow のデータ | collect 時 | 最新値の再発行 |
| stateIn() | Cold → Hot | ViewModel 内 | 状態保持と再発行 |
| shareIn() | Cold → Hot | 複数UIで共有 | イベント再発行 |
| retryWhen() | Flow | 条件付きリトライ | バックオフ付き再試行 |
実戦Tips
| シナリオ | 推奨設計 |
|---|---|
| API失敗を自動再試行したい | retryWhen |
| 画面再生成時に前回状態を再表示 | stateIn(replay=1) |
| 複数画面で最新イベント共有 | SharedFlow(replay=1) |
| UI側の手動リトライボタン | ViewModel に retryTrigger Flow を作る |
まとめ
| 概念 | 内容 |
|---|---|
| エラーリトライ | 一時的な失敗を再試行し、安定したデータ取得を実現 |
| リプレイ | 最新または過去の値を再発行して、UI復元を簡単に |
| 組み合わせると | 失敗に強く、再購読にも対応した堅牢な Flow 設計ができる |
Retry = 「再試行」
Replay = 「再発行」
Kotlin Flow の堅牢設計は、この2つの正しい理解から始まる。
Clean Architecture と組み合わせれば、安定 × 再現性の高い UI を実現できる。