0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlin Flow実践ノート:StateFlowとcollectの使い方

0
Last updated at Posted at 2025-10-26

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通信の現場でこの考えに救われました。
もし同じような悩みを持つ方がいれば、この記事が参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?