Kotlin Flow実践ノート:StateFlowとcollectの使い方
通信処理を Flow に置き換えると、よくある落とし穴があります。
「1発の通信要求を送ったのに、Flowのcollectが何度も反応する」
「一度だけ更新したい値が、複数回通知される」
この記事では、Kotlinの StateFlow を使うときに
誤信号(多重collect)を防ぐ実装構成を紹介します。
🧭 問題の背景
典型的な失敗パターンはこうです。
- Repository で
MutableStateFlowを公開 - Fragment や ViewModel が
collectを複数回呼ぶ - 画面再生成や再接続時に “複数のコレクター” が生まれる
結果として、通信結果が「1回」送信されたのに、UIが「2回以上」反応してしまう。
⚙️ 対策①:collectはライフサイクルを意識して1箇所に限定
FragmentやActivity側では、repeatOnLifecycle を使って
**「画面が有効な間だけ collect する」**のが安全です。
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.responseFlow.collect { response ->
// UI更新(多重collect防止済)
updateUi(response)
}
}
}
repeatOnLifecycle が自動でキャンセル&再起動してくれるため、
画面回転などで再生成されても collect の多重起動が起きません。
⚙️ 対策②:通信は suspend関数で“1発”制御
通信処理は Flow ではなく、まず “単発のsuspend関数” にします。
通信結果だけを Flow に流すことで、「一発通信・多発通知」を防ぎます。
class TcpRepository {
private val _responseFlow = MutableStateFlow<String?>(null)
val responseFlow: StateFlow<String?> = _responseFlow
suspend fun sendAndReceive(request: String) {
withContext(Dispatchers.IO) {
val socket = Socket("192.168.1.10", 4000)
socket.getOutputStream().write(request.toByteArray(Charsets.SJIS))
val buffer = ByteArray(1024)
val len = socket.getInputStream().read(buffer)
val response = buffer.copyOf(len).toString(Charsets.SJIS)
_responseFlow.value = response // Flow更新は1回だけ
socket.close()
}
}
}
→ 通信I/Oは suspend に切り離し、結果だけを StateFlow に送る。
これにより「Flowが多重collectされても通信は1発」で済みます。
⚙️ 対策③:再送防止のための Event ラッパ
Flowは「状態」を流すため、同じ値が再通知されることがあります。
その場合は「イベント型」クラスでラップします。
data class Event<out T>(private val content: T) {
private var handled = false
fun getIfNotHandled(): T? =
if (handled) null else content.also { handled = true }
}
ViewModel 側:
private val _eventFlow = MutableStateFlow<Event<String>?>(null)
val eventFlow = _eventFlow.asStateFlow()
fun updateOnce(msg: String) {
_eventFlow.value = Event(msg)
}
UI側:
viewModel.eventFlow.collect { event ->
event?.getIfNotHandled()?.let { showToast(it) }
}
これで「1回だけ通知されるイベント」が安全に扱えます。
🔍 まとめ:Flowを安全に使う3原則
| 原則 | 内容 |
|---|---|
| ① collectは1箇所に限定 |
repeatOnLifecycleで管理する |
| ② 通信処理はsuspendで単発化 | FlowにI/Oを埋め込まない |
| ③ 一度きりの通知はEventラッパ | 再通知による誤信号を防ぐ |
🪴 最後に
Flowは強力ですが、使い方を誤ると「通信が暴走」します。
大事なのは、“Flowを通信手段にしない” こと。
通信はコルーチンで、通知はFlowで。
私自身、USBシリアル通信やTCP通信の現場でこの考えに救われました。
もし同じような悩みを持つ方がいれば、この記事が参考になれば幸いです。