Sebelumnya kita mempelajari operator Flow. Bab ini menjawab dua pertanyaan mendasar tentang eksekusi Flow: di thread mana blok builder dijalankan, dan apa yang terjadi saat scope yang mengumpulkan Flow dibatalkan. Memahami kedua aspek ini penting agar pipeline Flow tetap aman saat berinteraksi dengan I/O berat, dan agar coroutine yang membatalkan diri tidak meninggalkan resource menggantung.
Aturan Context Preservation #
Aturan utama Flow: blok flow { ... } dijalankan di konteks pemanggil collect. Jika collect dipanggil dari Dispatchers.Main, blok builder juga berjalan di Dispatchers.Main. Hal ini disebut context preservation. Konsekuensinya, kode yang melakukan I/O blocking di dalam flow { ... } akan memblokir thread pemanggil — biasanya thread UI atau thread utama.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
println("=== 1. Default: flow berjalan di konteks pemanggil ===")
val flowSederhana = flow {
println("[flow] thread emit = ${namaPool(Thread.currentThread().name)}")
emit(1)
}
flowSederhana.collect {
println("[collect] thread = ${namaPool(Thread.currentThread().name)}; nilai=$it")
}
println()
println("=== 2. flowOn(Dispatchers.IO): upstream pindah ke IO ===")
val flowIO = flow {
println("[flow] thread emit = ${namaPool(Thread.currentThread().name)}")
emit("data-1")
emit("data-2")
}.flowOn(Dispatchers.IO)
flowIO.collect {
println("[collect] thread = ${namaPool(Thread.currentThread().name)}; nilai=$it")
}
println()
println("=== 3. Context preservation: collect tetap di pemanggil ===")
withContext(Dispatchers.Default) {
flow {
println("[flow] thread emit = ${namaPool(Thread.currentThread().name)}")
emit(42)
}.flowOn(Dispatchers.IO).collect {
println("[collect] thread = ${namaPool(Thread.currentThread().name)}; nilai=$it")
}
}
println()
println("=== 4. Cancellation: collect dibatalkan saat scope dibatalkan ===")
val hasil = withTimeoutOrNull(60) {
flow {
repeat(5) { i ->
emit(i)
delay(40) // total 200ms, lebih dari timeout 60ms
}
}.collect { println("[collect] terima $it") }
"selesai"
}
println("[main] hasil collect = $hasil (cancellationException ditangkap denganTimeoutOrNull)")
println()
println("=== 5. CancellationException di tengah flow ===")
val job = launch {
try {
flow {
repeat(10) { i ->
emit(i)
delay(30)
}
}.collect { println("[collect] $it") }
} catch (e: CancellationException) {
println("[flow-collect] dibatalkan: ${e::class.simpleName}")
}
}
delay(100) // biarkan beberapa emit terjadi
job.cancelAndJoin()
println("[main] job dibatalkan dan ditunggu")
}
fun namaPool(namaThread: String): String {
return when {
namaThread.startsWith("DefaultDispatcher") -> "DefaultDispatcher"
namaThread.startsWith("DefaultExecutor") -> "DefaultExecutor"
namaThread.contains("main") -> "main"
else -> namaThread
}
}Output yang Dihasilkan:
=== 1. Default: flow berjalan di konteks pemanggil ===
[flow] thread emit = main
[collect] thread = main; nilai=1
=== 2. flowOn(Dispatchers.IO): upstream pindah ke IO ===
[flow] thread emit = DefaultDispatcher
[collect] thread = main; nilai=data-1
[collect] thread = main; nilai=data-2
=== 3. Context preservation: collect tetap di pemanggil ===
[flow] thread emit = DefaultDispatcher
[collect] thread = DefaultDispatcher; nilai=42
=== 4. Cancellation: collect dibatalkan saat scope dibatalkan ===
[collect] terima 0
[collect] terima 1
[main] hasil collect = null (cancellationException ditangkap denganTimeoutOrNull)
=== 5. CancellationException di tengah flow ===
[collect] 0
[collect] 1
[collect] 2
[collect] 3
[flow-collect] dibatalkan: JobCancellationException
[main] job dibatalkan dan ditungguMari kita bedah bagian-bagian penting dari kode di atas:
- Bagian 1 (Default): tanpa
flowOn, blokflow { ... }berjalan di thread yang sama dengancollect. Output menunjukkan[flow] thread emit = maindan[collect] thread = main. Keduanya di threadmain. - Bagian 2 (
flowOn(Dispatchers.IO)): blokflow { ... }dipindahkan ke thread pool IO. Output membuktikan[flow] thread emit = DefaultDispatcher(Dispatchers.IO memakai thread dari DefaultDispatcher), tetapicollecttetap dimain. Inilah inti context preservation: upstream boleh pindah dispatcher, tetapicollecttidak. - Bagian 3 (
withContext(Dispatchers.Default)): ketikacollectdipanggil dari dalamwithContext, blokcollectikut pindah keDefaultDispatcher. Upstreamflow { ... }tetap di IO karenaflowOn. Output menunjukkan kedua jalur di thread pool yang berbeda. - Bagian 4 (
withTimeoutOrNull(60)): Flow dipaksa berhenti setelah 60ms. Karena tiap emit ditambahdelay(40)memakan lebih dari 60ms, hanya emit pertama (0) dan kedua (1) yang sempat dikumpulkan sebelumCancellationExceptiondilempar.withTimeoutOrNullmenangkap exception dan mengembalikannull.
flowOn dan Mekanisme Pengubahan Dispatcher #
Operator flowOn(dispatcher) memindahkan seluruh upstream ke dispatcher baru. Implementasinya: Flow membuat coroutine baru di dispatcher yang ditentukan, lalu mengirim nilai ke collector di konteks pemanggil melalui channel internal. Akibatnya, performa Flow yang sering memanggil flowOn sedikit lebih lambat karena ada context switch, tetapi keuntungannya jauh lebih besar: thread UI tidak terblokir saat Flow melakukan I/O berat.
| Skenario | Thread blok flow {} | Thread collect |
|---|---|---|
Tanpa flowOn | Sama dengan collect | Sama dengan pemanggil collect |
.flowOn(Dispatchers.IO) | Thread pool IO | Sama dengan pemanggil collect |
.flowOn(Dispatchers.Default) | Thread pool CPU | Sama dengan pemanggil collect |
Cancellation: Flow Menghormati CancellationException #
Flow dirancang untuk cooperative cancellation. Saat scope tempat collect berjalan dibatalkan, collect melempar CancellationException. Blok flow { ... } juga akan berhenti di titik suspend berikutnya (misalnya delay, emit, atau panggilan suspend lain).
Mekanisme ini berarti:
withTimeoutdanwithTimeoutOrNullbekerja langsung pada Flow tanpa kode tambahan.job.cancel()dari luar menghentikan Flow yang sedang berjalan.- Resource yang dibuka di dalam
flow { ... }sebaiknya ditutup dengantry { ... } finally { ... }agar tetap ditutup saat pembatalan terjadi.
Pada Bagian 5 kode contoh, kita melihat bahwa setelah job.cancelAndJoin() dipanggil, blok catch (e: CancellationException) menangkap exception, mencetak pesan, lalu Flow berakhir dengan rapi. Pola try/catch (CancellationException) berguna untuk log diagnostik atau pembersihan resource sebelum benar-benar berhenti.
Operator Cancellation-Specific #
Beberapa operator Flow berperilaku khusus terkait pembatalan:
| Operator | Perilaku saat dibatalkan |
|---|---|
flow { } builder | Menerima CancellationException di emit() atau delay() |
cancellable() | Menambahkan titik pemeriksaan pembatalan di setiap emit |
flowOn() | Meneruskan pembatalan ke upstream |
onCompletion { } | Tetap dipanggil baik sukses maupun dibatalkan |
catch { } | TIDAK menangkap CancellationException secara default (dilempar ulang) |
Operator cancellable() berguna saat upstream tidak secara native menghormati pembatalan — misalnya, flow { while(true) { emit(...) } } tanpa delay tidak akan merespons cancel() kecuali diberi .cancellable() sebelum collect.
Referensi Lanjutan #
- Flow
— panduan
flowOndan penanganan cancellation padaFlow - Coroutine context and dispatchers
— konteks dispatcher untuk
Flow
Selanjutnya #
Memahami context dan cancellation melengkapi gambaran teknis Flow. Bab berikutnya, Cold vs Hot Flows, akan membandingkan Flow (cold) dengan SharedFlow dan StateFlow (hot) — kapan memakai satu dan bukan yang lain, serta implikasi desain dari setiap pilihan.