Banyak library lama — driver database, SDK Firebase, atau API native — masih berbasis callback: Anda memanggil api.ambilData(key) { result -> ... } dan tidak bisa langsung menunggu hasil. Bab ini membahas cara mengubah API seperti itu menjadi suspend function dengan dua fungsi bantu: suspendCoroutine (versi dasar) dan suspendCancellableCoroutine (versi yang mendukung pembatalan). Dengan trik ini, kode callback yang berantakan dapat dirapikan menjadi baris suspend yang linear.
Mengapa Perlu Konversi #
Callback terasa natural untuk library async, tetapi cepat berantakan saat digabungkan:
api.ambilPengguna(id) { pengguna ->
api.ambilFoto(pengguna.id) { foto ->
api.ambilKota(pengguna.kotaId) { kota ->
// tiga level bersarang, susah dibaca
tampilkan(pengguna, foto, kota)
}
}
}Dengan konversi ke suspend function, kode yang sama menjadi linear:
suspend fun ambilProfil(id: Int): Profil = coroutineScope {
val pengguna = ambilPengguna(id)
val foto = async { ambilFoto(pengguna.id) }
val kota = async { ambilKota(pengguna.kotaId) }
Profil(pengguna, foto.await(), kota.await())
}suspendCoroutine: Konversi Dasar #
suspendCoroutine<T> { continuation -> ... } menghentikan coroutine saat ini sampai continuation.resume(value) atau continuation.resumeWithException(throwable) dipanggil. Di dalam blok, Anda memanggil API callback dan meneruskan continuation sebagai penerima hasil.
import kotlinx.coroutines.*
import kotlin.coroutines.*
// Simulasi API berbasis callback (mis. dari library lama)
interface CallbackApi {
fun ambilData(key: String, callback: (Result<String>) -> Unit)
fun batalkan()
}
// Implementasi tiruan: berhasil setelah jeda singkat
class CallbackApiTiruan : CallbackApi {
private var dibatalkan = false
override fun ambilData(key: String, callback: (Result<String>) -> Unit) {
println("[api] mulai ambil data untuk key=$key")
Thread {
Thread.sleep(30)
if (dibatalkan) {
println("[api] permintaan dibatalkan sebelum selesai")
return@Thread
}
val hasil = "data-untuk-$key"
println("[api] selesai, kirim hasil: $hasil")
callback(Result.success(hasil))
}.start()
}
override fun batalkan() {
println("[api] permintaan dibatalkan")
dibatalkan = true
}
}
// Versi 1: suspendCoroutine (tidak mendukung pembatalan)
suspend fun ambilDataSuspend(api: CallbackApi, key: String): String =
suspendCoroutine { kontinuation ->
api.ambilData(key) { result ->
result.onSuccess { kontinuation.resume(it) }
.onFailure { kontinuation.resumeWithException(it) }
}
}
// Versi 2: suspendCancellableCoroutine (mendukung pembatalan)
suspend fun ambilDataCancellable(api: CallbackApi, key: String): String =
suspendCancellableCoroutine { kontinuation ->
api.ambilData(key) { result ->
result.onSuccess { kontinuation.resume(it) }
.onFailure { kontinuation.resumeWithException(it) }
}
kontinuation.invokeOnCancellation { api.batalkan() }
}
fun main() = runBlocking {
val api = CallbackApiTiruan()
println("=== 1. suspendCoroutine: sukses ===")
val hasil1 = ambilDataSuspend(api, "Budi")
println("[main] hasil = $hasil1")
println()
println("=== 2. suspendCancellableCoroutine: sukses ===")
val api2 = CallbackApiTiruan()
val hasil2 = ambilDataCancellable(api2, "Siti")
println("[main] hasil = $hasil2")
println()
println("=== 3. Pembatalan: withTimeout memicu invokeOnCancellation ===")
val api3 = CallbackApiTiruan()
val hasil3 = withTimeoutOrNull(15) {
ambilDataCancellable(api3, "Citra") // ambil butuh 30ms, timeout 15ms
}
println("[main] hasil = $hasil3 (null karena dibatalkan)")
delay(50) // beri waktu thread API menyelesaikan dan mencetak pesan
println()
println("=== 4. Exception dirambatkan dari callback ===")
class ApiError : CallbackApi {
override fun ambilData(key: String, callback: (Result<String>) -> Unit) {
callback(Result.failure(IllegalStateException("koneksinya gagal")))
}
override fun batalkan() {}
}
val apiErr = ApiError()
try {
ambilDataCancellable(apiErr, "Dewi")
} catch (e: IllegalStateException) {
println("[main] tangkap: ${e.message}")
}
println()
println("=== 5. Banyak permintaan paralel dengan async ===")
val api5 = CallbackApiTiruan()
val hasil5 = listOf("k1", "k2", "k3").map { k -> async { ambilDataCancellable(api5, k) } }
.awaitAll()
println("[main] semua hasil = $hasil5")
}Output yang Dihasilkan:
=== 1. suspendCoroutine: sukses ===
[api] mulai ambil data untuk key=Budi
[api] selesai, kirim hasil: data-untuk-Budi
[main] hasil = data-untuk-Budi
=== 2. suspendCancellableCoroutine: sukses ===
[api] mulai ambil data untuk key=Siti
[api] selesai, kirim hasil: data-untuk-Siti
[main] hasil = data-untuk-Siti
=== 3. Pembatalan: withTimeout memicu invokeOnCancellation ===
[api] mulai ambil data untuk key=Citra
[api] permintaan dibatalkan
[main] hasil = null (null karena dibatalkan)
[api] permintaan dibatalkan sebelum selesai
=== 4. Exception dirambatkan dari callback ===
[main] tangkap: koneksinya gagal
=== 5. Banyak permintaan paralel dengan async ===
[api] mulai ambil data untuk key=k1
[api] mulai ambil data untuk key=k2
[api] mulai ambil data untuk key=k3
[api] selesai, kirim hasil: data-untuk-k1
[api] selesai, kirim hasil: data-untuk-k2
[api] selesai, kirim hasil: data-untuk-k3
[main] semua hasil = [data-untuk-k1, data-untuk-k2, data-untuk-k3]Mari kita bedah bagian-bagian penting:
interface CallbackApi: API hipotetis dengan metodeambilData(key, callback). Callback menerimaResult<String>untuk mempersatukan jalur sukses dan gagal. ImplementasiCallbackApiTiruanmenjalankan operasi di thread terpisah denganThread.sleep(30)— simulasi latensi API nyata.ambilDataSuspend(suspendCoroutine): konversi paling sederhana. BloksuspendCoroutinemenerimakontinuationyang berperan sebagai jembatan antara callback dan coroutine. Saat callback dipanggil, kita teruskan hasil kekontinuation.resume(value)atau exception kekontinuation.resumeWithException(throwable). FungsiambilDataSuspendsendiri tidak bisa membatalkan API — bila coroutine dibatalkan, API tetap berjalan.ambilDataCancellable(suspendCancellableCoroutine): versi yang mendukung pembatalan. Bariskontinuation.invokeOnCancellation { api.batalkan() }mendaftarkan handler yang dipanggil saat coroutine dibatalkan — di sinilah kita memanggilapi.batalkan()untuk benar-benar menghentikan operasi di sisi API.- Bagian 3 (
withTimeoutOrNull(15)): timeout 15ms lebih pendek dari 30ms API. Saat timeout terpicu,suspendCancellableCoroutinemembatalkan continuation, yang memicuinvokeOnCancellation, yang memanggilapi.batalkan(). Output membuktikan[api] permintaan dibatalkanmuncul — pembatalan dirambatkan ke API. Thread API kemudian bangun, melihat flagdibatalkan = true, dan mencetakpermintaan dibatalkan sebelum selesaitanpa memanggil callback. - Bagian 4 (exception): callback yang memanggil
Result.failure(...)dirambatkan sebagai exception dari suspend function. Kode pemanggil dapattry/catchseperti biasa. - Bagian 5 (
async { ... }.awaitAll()): kombinasi suspend function denganasyncmemungkinkan permintaan paralel. Output menunjukkan[api] mulai ambil datamuncul tiga kali hampir bersamaan — ketiga thread API berjalan paralel.awaitAllmenunggu semua selesai.
Aturan Penting resume #
Ada beberapa aturan yang harus dipatuhi saat memakai suspendCoroutine / suspendCancellableCoroutine:
| Aturan | Konsekuensi bila dilanggar |
|---|---|
resume(...) dipanggil tepat sekali | Resume kedua melempar IllegalStateException |
resumeWithException(...) dipanggil tepat sekali | Sama seperti di atas |
Jangan menyimpan continuation di field yang long-lived | Memory leak |
Panggil invokeOnCancellation di akhir blok (sebelum return) | Handler tidak terdaftar |
Pola Umum: Susun sebagai Extension Function #
Idiom yang sering dipakai: bungkus API pihak ketiga sebagai extension function suspend agar pemanggilan terasa natural:
suspend fun CallbackApi.ambilDataOrNull(key: String): String? =
suspendCancellableCoroutine { kontinuation ->
ambilData(key) { result ->
result.onSuccess { kontinuation.resume(it) }
.onFailure { kontinuation.resumeWithException(it) }
}
invokeOnCancellation { batalkan() }
}Pemanggilan menjadi:
val data = api.ambilDataOrNull("kunci") ?: return
println(data)Referensi Lanjutan #
- Coroutine basics
— dasar mengganti callback dengan
suspendfunction - Composing suspending functions
— pola
suspendCoroutinedan bridge callback
Selanjutnya #
Konversi callback ke coroutine selesai. Bab berikutnya, Coroutine Testing, akan membahas pengujian coroutine itu sendiri: bagaimana menulis tes yang menjalankan suspend function dengan runBlocking, menggunakan withTimeout untuk mencegah tes menggantung, dan memverifikasi exception serta pembatalan.