Tanpa scope, coroutine mudah bocor — sebuah launch yang lupa di-join() akan terus berjalan di latar belakang, atau bahkan menggagalkan program lewat exception yang tidak tertangkap. CoroutineScope adalah abstraksi yang menyelesaikan masalah ini dengan menyediakan batas eksplisit untuk siklus hidup coroutine. Chapter ini membahas apa itu scope, bagaimana builder coroutineScope { } bekerja, dan mengapa setiap coroutine yang kita tulis harus berada di dalam sebuah scope.
Apa itu CoroutineScope #
CoroutineScope adalah sebuah interface yang menyimpan sebuah CoroutineContext dan menjamin bahwa semua coroutine yang dimulai di dalamnya terikat pada konteks tersebut. Lebih penting lagi, sebuah scope menentukan lifetime dari coroutine-coroutine anaknya: ketika scope berakhir, semua coroutine anaknya akan dibatalkan.
Dua cara umum untuk mendapatkan scope:
| Cara | Sifat | Kapan dipakai |
|---|---|---|
Builder coroutineScope { ... } | Memblokade sampai semua anak selesai | Memisahkan region komputasi; fungsi suspend publik |
Builder runBlocking { ... } | Memblokade thread pemanggil | Entry-point main() dan beberapa test |
Properti CoroutineScope(context) | Tidak otomatis menunggu | Kelas yang memegang lifecycle (mis. Android ViewModel) |
Di dalam blok coroutineScope, kita mendapatkan this: CoroutineScope. Semua pemanggilan launch, async, atau coroutineScope di dalam blok otomatis menjadi anak dari scope ini.
coroutineScope Builder #
Builder coroutineScope { ... } adalah suspend function yang membuat scope baru, menjalankan blok di dalamnya, dan tidak kembali sampai semua coroutine anak selesai. Builder ini mengembalikan nilai ekspresi terakhir blok.
import kotlinx.coroutines.*
fun main() = runBlocking {
println("[outer] mulai")
// coroutineScope membatasi siklus hidup anak
val total = coroutineScope {
println(" [scope] mulai")
val a = launch {
repeat(2) { i ->
println(" [anak-a] tugas-$i")
delay(30)
}
}
val b = async {
repeat(2) { i ->
println(" [anak-b] tugas-$i")
delay(20)
}
"hasil-b"
}
a.join()
val hasilB = b.await()
println(" [scope] selesai, hasil=$hasilB")
"OK"
}
println("[outer] total=$total")
println("[outer] selesai")
}Output yang Dihasilkan:
[outer] mulai
[scope] mulai
[anak-a] tugas-0
[anak-b] tugas-0
[anak-b] tugas-1
[anak-a] tugas-1
[scope] selesai, hasil=hasil-b
[outer] total=OK
[outer] selesaiMari kita telaah baris-baris kode di atas:
- Baris 3 (
runBlocking { ... }):runBlockingdimain()menyediakan scope terluar. Tanpa ini, kita tidak bisa memanggilcoroutineScopedi dalammain(). - Baris 7 (
coroutineScope { ... }): builder membuat scope baru. Kode di dalam blok berjalan sebagai satu kesatuan — builder tidak akan kembali sebelum semualaunch/asyncdi dalamnya selesai. - Baris 10-16 (
launch { ... }): coroutine anakamencetak dua pesan dengan jeda 30ms. Karenalaunchtidak mengembalikan nilai, kita butuha.join()untuk menunggu selesai. - Baris 17-23 (
async { ... }): coroutine anakbjuga mencetak dua pesan lalu mengembalikan"hasil-b". TipeDeferred<String>memungkinkan kita mengambil nilai viaawait(). - Baris 24-26:
a.join()danb.await()dipanggil secara berurutan. Karenaadanbsudah dimulai lebih awal, keduanya berjalan tumpang tindih — total waktu sekitar 60ms (durasia), bukan 100ms. - Baris 27 (
"OK"): ekspresi terakhir di blokcoroutineScopemenjadi nilai kembalian builder. Di sini, kembalian berupa string"OK".
Scope Otomatis Membersihkan #
Salah satu jaminan utama coroutineScope: ketika blok selesai (normal atau karena exception), semua coroutine anak yang masih berjalan akan dibatalkan. Ini mencegah kebocoran coroutine.
suspend fun proses() = coroutineScope {
launch {
repeat(100) { i ->
delay(50)
println("tugas-$i")
}
}
delay(120) // selesai lebih awal
// setelah blok ini selesai, launch di atas dibatalkan
}Setelah 120ms berlalu, blok coroutineScope selesai. Coroutine launch yang baru mencetak tugas-2 otomatis dibatalkan oleh scope — tidak perlu cancel() manual.
Scope Kustom #
Untuk kelas yang memerlukan lifecycle terkelola, kita bisa membuat CoroutineScope sebagai properti. Pola ini umum di backend dan Android:
class Worker : CoroutineScope {
override val coroutineContext: CoroutineContext =
SupervisorJob() + Dispatchers.Default
fun mulai() {
launch { println("tugas 1 jalan") }
launch { println("tugas 2 jalan") }
}
fun hentikan() {
coroutineContext.cancel()
}
}Referensi Lanjutan #
- Coroutine context and dispatchers
— panduan
CoroutineScopedan structured concurrency - Coroutine basics — konteks dasar scope coroutine
Selanjutnya #
Dengan memahami scope, kita siap membahas prinsip yang mengikat scope dan coroutine anak: structured concurrency. Bab berikutnya akan menunjukkan bagaimana hubungan induk-anak antar scope menjaga kode tetap aman dan terkontrol bahkan ketika exception terjadi.