Sejauh ini kita sudah memakai Flow, SharedFlow, dan StateFlow tanpa benar-benar membedakan kapan memakai yang mana. Bab ini memperjelas perbedaan fundamental antara cold dan hot stream di Kotlin, menjelaskan mengapa Flow dijalankan ulang setiap kali di-collect sementara SharedFlow selalu aktif terlepas dari collector. Pemahaman ini krusial untuk memilih abstraksi yang tepat saat merancang state atau event di aplikasi nyata.

Apa itu Cold Stream #

Cold stream adalah stream yang baru mulai menghasilkan nilai saat di-collect. Setiap kali sebuah cold Flow di-collect, blok builder dieksekusi dari awal. Tidak ada state yang dibagikan antar collector — masing-masing mendapat salinan terpisah.

Karakteristik cold Flow:

  • Lazy: tidak ada komputasi sampai ada collector.
  • Re-playable: collect ulang selalu memberi hasil yang sama (jika builder deterministik).
  • Single execution: setiap collect menjalankan blok dari awal.
  • Tidak ada efek samping sampai di-collect.

Apa itu Hot Stream #

Hot stream adalah stream yang aktif terlepas dari collector. SharedFlow dan StateFlow adalah implementasi hot stream di kotlinx.coroutines. Saat sebuah nilai di-emit, semua collector yang aktif akan menerimanya — tetapi collector yang belum mendaftar akan kehilangan nilai yang sudah lewat.

Karakteristik hot stream:

  • Aktif sebelum collector: emit dapat terjadi bahkan tanpa collector.
  • Shared antar collector: setiap nilai dipancarkan ke semua collector aktif.
  • Stateful: hot stream bisa punya nilai terbaru (StateFlow) atau tidak (SharedFlow).

Perbandingan Praktis #

Tabel berikut merangkum perbedaan utama:

AspekFlow (cold)SharedFlow (hot)StateFlow (hot)
MulaiSaat di-collectSaat di-emitSaat di-emit atau dibuat
EksekusiPer collectorShared antar collectorShared antar collector
ReplayTidak adaConfigurable (default 0)Selalu 1 (nilai terbaru)
Nilai awalTidak adaTidak ada (kecuali replay > 0)Wajib ada
Cocok untukSumber data, pipelineEvent broadcastState UI

Demo: Cold Flow dieksekusi ulang #

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    println("=== 1. Cold Flow: dieksekusi ulang tiap collect ===")
    val coldFlow = flow {
        println("[cold] mulai emit")
        emit("A")
        emit("B")
        emit("C")
        println("[cold] selesai emit")
    }

    println("-- collect pertama --")
    coldFlow.collect { println("  terima: $it") }
    println("-- collect kedua (flow dijalankan ulang) --")
    coldFlow.collect { println("  terima: $it") }

    println()
    println("=== 2. Hot SharedFlow: nilai dibagikan ke semua collector ===")
    val shared = MutableSharedFlow<String>(replay = 0)
    val jobPemancar = launch {
        var i = 0
        while (true) {
            val pesan = "pesan-${i}"
            println("[pemancar] kirim $pesan")
            shared.emit(pesan)
            delay(50)
            i++
        }
    }

    // Collector pertama
    val jobCollector1 = launch {
        shared.collect { println("  [c1] terima: $it") }
    }
    delay(80)  // biarkan beberapa pesan masuk ke collector 1

    // Collector kedua bergabung belakangan
    println("-- collector 2 bergabung --")
    val jobCollector2 = launch {
        shared.collect { println("  [c2] terima: $it") }
    }
    delay(120)

    jobCollector1.cancel()
    jobCollector2.cancel()
    jobPemancar.cancel()
    println("[main] semua job dihentikan")

    println()
    println("=== 3. Hot StateFlow: selalu punya nilai terbaru ===")
    val state = MutableStateFlow(0)
    println("[main] state awal = ${state.value}")

    val jobState = launch {
        state.collect { println("  [state] nilai = $it") }
    }
    delay(30)
    state.value = 10
    delay(30)
    state.value = 20
    delay(30)
    jobState.cancel()
    println("[main] state akhir = ${state.value}")

    println()
    println("=== 4. Cold Flow: tidak ada efek samping sampai di-collect ===")
    var jumlahEmit = 0
    val flowPemalas = flow {
        jumlahEmit++
        emit(jumlahEmit)
    }
    println("[main] sebelum collect, jumlahEmit = $jumlahEmit")
    flowPemalas.collect { println("  [collect] terima $it") }
    println("[main] setelah collect pertama, jumlahEmit = $jumlahEmit")
    flowPemalas.collect { println("  [collect] terima $it") }
    println("[main] setelah collect kedua, jumlahEmit = $jumlahEmit")
}

Output yang Dihasilkan:

text
=== 1. Cold Flow: dieksekusi ulang tiap collect ===
-- collect pertama --
[cold] mulai emit
  terima: A
  terima: B
  terima: C
[cold] selesai emit
-- collect kedua (flow dijalankan ulang) --
[cold] mulai emit
  terima: A
  terima: B
  terima: C
[cold] selesai emit

=== 2. Hot SharedFlow: nilai dibagikan ke semua collector ===
[pemancar] kirim pesan-0
[pemancar] kirim pesan-1
  [c1] terima: pesan-1
-- collector 2 bergabung --
[pemancar] kirim pesan-2
  [c1] terima: pesan-2
  [c2] terima: pesan-2
[pemancar] kirim pesan-3
  [c1] terima: pesan-3
  [c2] terima: pesan-3
[main] semua job dihentikan

=== 3. Hot StateFlow: selalu punya nilai terbaru ===
[main] state awal = 0
  [state] nilai = 0
  [state] nilai = 10
  [state] nilai = 20
[main] state akhir = 20

=== 4. Cold Flow: tidak ada efek samping sampai di-collect ===
[main] sebelum collect, jumlahEmit = 0
  [collect] terima 1
[main] setelah collect pertama, jumlahEmit = 1
  [collect] terima 2
[main] setelah collect kedua, jumlahEmit = 2

Mari kita bedah bagian penting dari kode di atas:

  1. Bagian 1 (Cold Flow di-collect dua kali): output menunjukkan blok flow { } dijalankan ulang pada collect kedua. Pesan [cold] mulai emit muncul dua kali — sekali untuk setiap collect. Tidak ada state yang dipertahankan antar collect.
  2. Bagian 2 (SharedFlow dengan dua collector): pemancar memancarkan pesan tiap 50ms. Collector 1 mendaftar pertama dan menerima pesan-1, pesan-2, dst. Collector 2 bergabung belakangan dan hanya menerima pesan yang dipancarkan setelah ia mendaftar. pesan-0 hilang untuk Collector 2 karena replay = 0 — inilah inti hot stream: nilai yang sudah lewat tidak diulang.
  3. Bagian 3 (StateFlow dengan nilai terbaru): saat collector mendaftar, ia langsung menerima nilai terbaru (0). Setiap update state.value = 10 dan state.value = 20 langsung diterima collector. state.value juga bisa dibaca langsung tanpa collect — fitur penting StateFlow untuk membaca state sinkron.
  4. Bagian 4 (Efek samping cold Flow): variabel jumlahEmit hanya bertambah saat Flow di-collect. Sebelum collect, nilainya 0. Setelah collect pertama, nilainya 1. Setelah collect kedua, nilainya 2. Ini membuktikan bahwa builder Flow benar-benar lazy — efek samping tidak terjadi sampai collect dipanggil.

Kapan Memakai Cold Flow #

Cold Flow cocok untuk:

  • Sumber data yang bisa dimulai ulang: query database, request HTTP, baca file. Setiap collect memicu eksekusi baru.
  • Pipeline transformasi fungsional: flowOf(...).map { }.filter { }.collect { } — sifat lazy membuat pipeline mudah dibaca.
  • Operasi yang harus deterministik: bila hasil collect harus sama setiap kali (jika input sama), Flow adalah pilihan yang tepat.
  • Resource cleanup otomatis: Flow yang dihentikan (cancel) otomatis membersihkan resource di akhir blok builder.

Kapan Memakai Hot Stream #

Hot stream cocok untuk:

  • Event broadcast: tombol diklik, pesan WebSocket masuk, perubahan preferensi. Multiple subscriber menerima event yang sama.
  • State UI yang berubah: StateFlow adalah pilihan standar di ViewModel Android untuk menampung state UI.
  • Sinkronisasi antar komponen: hot stream memungkinkan komponen berbeda membaca state yang sama.
  • Sumber yang aktif terus-menerus: sensor, timer, atau queue yang aktif bahkan tanpa subscriber.

SharedFlow vs StateFlow #

StateFlow adalah spesialisasi SharedFlow dengan replay = 1 dan conflation otomatis (nilai baru menimpa nilai lama yang belum dikonsumsi). Perbandingan:

AspekSharedFlowStateFlow
Nilai awalOpsionalWajib
ReplayConfigurableSelalu 1
ConflationManual via conflate()Otomatis
Akses nilai tanpa collectTidak (value tidak ada)Ya (state.value)
IdempotencyEmit sama tetap dipancarkanEmit nilai sama diabaikan

Referensi Lanjutan #

  • Flow — pembedaan Flow (cold) vs SharedFlow/StateFlow (hot)
  • Channels — alternatif hot stream via Channel

Selanjutnya #

Sekarang setelah kita paham kapan memakai Flow vs SharedFlow vs StateFlow, bab berikutnya akan membahas cara menguji Flow. Kita akan melihat pola flow.toList(...) untuk menangkap semua emit ke List, lalu memverifikasi urutan dan isi dengan assertion. Pendekatan ini memungkinkan pengujian Flow tanpa dependensi tambahan.