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:

CaraSifatKapan dipakai
Builder coroutineScope { ... }Memblokade sampai semua anak selesaiMemisahkan region komputasi; fungsi suspend publik
Builder runBlocking { ... }Memblokade thread pemanggilEntry-point main() dan beberapa test
Properti CoroutineScope(context)Tidak otomatis menungguKelas 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.

kotlin
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:

text
[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] selesai

Mari kita telaah baris-baris kode di atas:

  1. Baris 3 (runBlocking { ... }): runBlocking di main() menyediakan scope terluar. Tanpa ini, kita tidak bisa memanggil coroutineScope di dalam main().
  2. Baris 7 (coroutineScope { ... }): builder membuat scope baru. Kode di dalam blok berjalan sebagai satu kesatuan — builder tidak akan kembali sebelum semua launch/async di dalamnya selesai.
  3. Baris 10-16 (launch { ... }): coroutine anak a mencetak dua pesan dengan jeda 30ms. Karena launch tidak mengembalikan nilai, kita butuh a.join() untuk menunggu selesai.
  4. Baris 17-23 (async { ... }): coroutine anak b juga mencetak dua pesan lalu mengembalikan "hasil-b". Tipe Deferred<String> memungkinkan kita mengambil nilai via await().
  5. Baris 24-26: a.join() dan b.await() dipanggil secara berurutan. Karena a dan b sudah dimulai lebih awal, keduanya berjalan tumpang tindih — total waktu sekitar 60ms (durasi a), bukan 100ms.
  6. Baris 27 ("OK"): ekspresi terakhir di blok coroutineScope menjadi 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.

kotlin Contoh: scope membatalkan anak yang masih berjalan
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:

kotlin Contoh: kelas yang memegang scope sendiri
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 #

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.