Sampai di sini kita sudah memakai runBlocking, launch, dan async tanpa membahas perbedaannya secara formal. Ketiganya adalah coroutine builder — fungsi yang membuat coroutine baru dari sebuah blok suspend. Memilih builder yang salah dapat menyebabkan bug halus: hasil yang tidak ditunggu, thread yang terblokade, atau exception yang terlempar ke tempat tak terduga. Chapter ini membedah ketiga builder utama, kapan memakai masing-masing, dan jebakan umum yang sering dijumpai.

Tiga Builder Utama #

Kotlin coroutines menyediakan banyak builder, tetapi tiga yang paling sering dipakai:

BuilderMengembalikanMemblokade thread?Kapan dipakai
runBlockingNilai blok TYa, sampai blok selesaiEntry-point JVM (main, test); menjembatani kode non-coroutine
launchJobTidakFire-and-forget; tugas tanpa nilai kembalian
asyncDeferred<T>TidakKomputasi yang menghasilkan nilai; konkurensi paralel

runBlocking adalah satu-satunya builder yang sengaja memblokade thread pemanggil. launch dan async segera kembali setelah menjadwalkan coroutine — pemanggil harus secara eksplisit menunggu jika perlu (lewat join() untuk launch, atau await() untuk async).

Contoh Bertiga dalam Satu Kode #

Kode berikut mendemonstrasikan ketiga builder dalam satu program:

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 1. runBlocking: memblokade thread sampai blok selesai
    println("[runBlocking] mulai")
    val total = runBlocking {
        delay(50)
        42
    }
    println("[runBlocking] hasil=$total")

    // 2. launch: fire-and-forget, mengembalikan Job
    println("[launch] mulai")
    val job: Job = launch {
        repeat(3) { i ->
            println("  launch tugas-$i")
            delay(30)
        }
    }
    job.join()
    println("[launch] selesai")

    // 3. async: mengembalikan Deferred<T> yang bisa di-await
    println("[async] mulai")
    val deferredA: Deferred<String> = async {
        delay(40)
        "hasil-A"
    }
    val deferredB: Deferred<Int> = async {
        delay(30)
        7
    }
    val hasil = "${deferredA.await()} + ${deferredB.await()}"
    println("[async] hasil=$hasil")
}

Output yang Dihasilkan:

text
[runBlocking] mulai
[runBlocking] hasil=42
[launch] mulai
  launch tugas-0
  launch tugas-1
  launch tugas-2
[launch] selesai
[async] mulai
[async] hasil=hasil-A + 7

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (fun main() = runBlocking { ... }): runBlocking di sini membungkus seluruh main() agar program tidak keluar sebelum semua coroutine anak selesai. Tanpa runBlocking terluar, main() dapat selesai dan JVM berhenti sebelum launch atau async selesai.
  2. Baris 6-9 (runBlocking { delay(50); 42 }): runBlocking bersarang mengembalikan nilai ekspresi terakhir (42). Pola ini jarang dipakai di produksi karena memblokade thread; biasanya dipakai di unit test untuk verifikasi.
  3. Baris 13 (launch { ... }): builder launch menjalankan tiga iterasi dengan delay(30). Builder ini tidak mengembalikan nilai — yang dikembalikan adalah Job untuk kontrol lifecycle.
  4. Baris 19 (job.join()): menunggu launch selesai sebelum lanjut ke blok async. Tanpa join, urutan output bisa berbeda.
  5. Baris 23-25 (async { ... }): async seperti launch tetapi mengembalikan Deferred<T>. Tipe T ditentukan dari ekspresi terakhir blok — "hasil-A" jadi Deferred<String>, 7 jadi Deferred<Int>.
  6. Baris 28 (deferredA.await()): await() adalah suspend function yang menangguhkan sampai hasil tersedia. Karena async sudah dimulai lebih awal, delay(40) di A dan delay(30) di B berjalan tumpang tindih — total waktu hanya ~40ms, bukan 70ms.

Kapan Memakai Apa #

Pilihan builder menentukan struktur kode. Berikut panduan praktis:

  • runBlocking: hanya untuk entry-point dan test. Hindari memakainya di dalam kode library karena akan memblokade thread pemanggil.
  • launch: untuk side-effect — menulis ke log, memperbarui UI, menyimpan ke database. Pemanggil tidak peduli nilai kembalian.
  • async: untuk komputasi yang menghasilkan nilai. Pemanggil perlu menunggu hasil lewat await().
kotlin Contoh: pola umum untuk komputasi paralel
suspend fun ambilProfil(id: Int): Profil = coroutineScope {
    val a = async { ambilPengguna(id) }
    val b = async { ambilFoto(id) }
    val c = async { ambilKota(id) }
    Profil(a.await(), b.await(), c.await())
}

Pola di atas adalah idiom standar: tiga async dimulai bersamaan, lalu ketiganya di-await secara berurutan. Total waktu mendekati maksimum dari tiga operasi, bukan jumlah ketiganya.

Jebakan: async tanpa await #

Salah satu kesalahan paling umum adalah membuat async dan langsung membuang referensinya:

kotlin Anti-pattern: async yang dibuang
suspend fun prosesSalah(id: Int) {
    async { ambilData(id) }  // hasil dibuang, exception terlempar ke scope
    println("ini jalan dulu")
}

Jika blok async melempar exception, exception tersebut akan terpropagasi ke scope induk (membatalkan coroutine saudara). Ini sering tidak diinginkan. Solusinya: selalu await() agar exception muncul di tempat yang diharapkan, atau ganti ke launch jika tidak butuh nilai.

Referensi Lanjutan #

Selanjutnya #

Setelah mengenal builder, pertanyaan berikutnya adalah: bagaimana lifecycle coroutine dikelola agar tidak ada yang bocor? Jawabannya ada pada CoroutineScope — yang akan kita bahas pada chapter Coroutine Scope.