Operasi asinkron sudah menjadi bagian tak terpisahkan dari aplikasi Android modern: panggilan jaringan, query database, parsing JSON berukuran besar — semuanya berpotensi memblokir thread UI jika dijalankan synchronously. Kotlin Coroutines menawarkan cara menulis kode asinkron yang terlihat seperti kode biasa (sequential-looking). Pada bab ini kita melihat bagaimana viewModelScope dan lifecycleScope menjadi scope default di Android, serta kapan harus memindahkan eksekusi ke Dispatchers.IO.
viewModelScope dan lifecycleScope #
viewModelScope adalah CoroutineScope yang terikat ke ViewModel. Scope ini otomatis dibatalkan saat ViewModel.onCleared() dipanggil, sehingga kita tidak perlu membatalkan job manual. lifecycleScope adalah scope yang terikat ke LifecycleOwner (Activity/Fragment), dibatalkan saat lifecycle DESTROYED.
class TugasViewModel(private val repo: TugasRepository) : ViewModel() {
private val _state = MutableStateFlow<List<String>>(emptyList())
val state: StateFlow<List<String>> = _state.asStateFlow()
fun muat() {
viewModelScope.launch {
_state.value = repo.muatTugas() // suspend sampai selesai
}
}
}Fungsi launch menjadwalkan coroutine di dispatcher default scope (Dispatchers.Main untuk viewModelScope). Karena repo.muatTugas() adalah suspend, ia memblokir coroutine tanpa memblokir thread — thread bisa dipakai untuk coroutine lain.
Dispatchers.Main vs IO #
Dispatcher menentukan thread pool tempat coroutine berjalan. Dispatchers.Main adalah thread UI Android (disediakan oleh kotlinx-coroutines-android); cocok untuk operasi ringan seperti memperbarui state, menggabungkan data, dan memicu recomposition. Dispatchers.IO adalah pool yang dioptimalkan untuk operasi I/O blocking (network, disk); memiliki 64 thread default.
suspend fun muatPengguna(id: Int): Pengguna = withContext(Dispatchers.IO) {
api.fetchPengguna(id) // blocking HTTP call aman di IO pool
}Fungsi withContext(Dispatchers.IO) mengalihkan eksekusi ke IO pool untuk blok tersebut, lalu kembali ke dispatcher asal. Pola ini disebut main-safe — pemanggil tidak perlu tahu bahwa fungsi ini melakukan I/O blocking, ia cukup memanggil sebagai suspend function biasa.
Retrofit + Coroutines #
Retrofit sejak versi 2.6 mendukung suspend function pada interface service. Kita tidak lagi memakai Call<T> secara manual — Retrofit men-generate implementasi yang mengembalikan hasil langsung.
interface PenggunaApi {
@GET("pengguna/{id}")
suspend fun fetchPengguna(@Path("id") id: Int): Pengguna
}
class PenggunaRepository(private val api: PenggunaApi) {
suspend fun muatPengguna(id: Int): Pengguna = api.fetchPengguna(id)
}Pada ViewModel, kita cukup memanggil repo.muatPengguna(id) di dalam viewModelScope.launch. Exception yang dilempar oleh Retrofit (mis. HttpException) dapat ditangkap dengan try/catch biasa.
Simulasi Coroutine di Android #
Cuplikan berikut memodelkan ViewModel, Repository, dan ApiService. Karena Dispatchers.Main membutuhkan kotlinx-coroutines-android (tidak tersedia di JVM murni), simulasi ini memakai Dispatchers.Default sebagai pengganti Main. Perilaku (suspend, dispatcher switch, dan lifecycle cancellation) tetap sama:
// coroutines_di_android.kt
// Mendemonstrasikan viewModelScope.launch, lifecycleScope.launch, dan
// pengaturan dispatcher (Dispatchers.Main vs IO) untuk Retrofit + coroutines.
//
// Catatan: Dispatchers.Main membutuhkan kotlinx-coroutines-android (tidak
// tersedia di JVM murni). Pada contoh ini kita memakai Dispatchers.Default
// sebagai pengganti Main agar simulasi berjalan tanpa Android SDK. Perilaku
// tetap sama: dispatcher default untuk operasi ringan, IO untuk network.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
// Mock Retrofit service
class ApiService {
suspend fun fetchPengguna(id: Int): String {
println(" [IO] GET /pengguna/$id dimulai")
delay(50) // simulasi network
println(" [IO] GET /pengguna/$id selesai")
return "Budi-$id"
}
}
class Repository(private val api: ApiService) {
suspend fun muatPengguna(id: Int): String = api.fetchPengguna(id)
}
class ViewModel(private val repo: Repository) {
// viewModelScope — otomatis dibatalkan saat ViewModel.onCleared()
// Dispatchers.Default dipakai di sini sebagai pengganti Dispatchers.Main
val scope = CoroutineScope(Dispatchers.Default)
fun muat(id: Int, onResult: (String) -> Unit) {
scope.launch {
println(" [Main] sebelum fetch")
val hasil = repo.muatPengguna(id) // suspend sampai selesai
println(" [Main] setelah fetch")
onResult(hasil)
}
}
}
fun main() = runBlocking {
val api = ApiService()
val repo = Repository(api)
val vm = ViewModel(repo)
println("=== viewModelScope.launch(Dispatchers.Main) ===")
val hasil = mutableListOf<String>()
// Tiga request berurutan agar output deterministik
vm.muat(1) { hasil.add(it); println(" onResult: $it") }
delay(80)
vm.muat(2) { hasil.add(it); println(" onResult: $it") }
delay(80)
vm.muat(3) { hasil.add(it); println(" onResult: $it") }
// Tunggu semua launch selesai
delay(100)
println("\nHasil dikumpulkan: $hasil")
println("\n=== Demonstrasi dispatcher IO ===")
val ioScope = CoroutineScope(Dispatchers.IO)
val job = ioScope.launch {
println(" [IO] berjalan di thread=${Thread.currentThread().name}")
}
job.join()
println("Selesai")
}Output yang Dihasilkan:
=== viewModelScope.launch(Dispatchers.Main) ===
[Main] sebelum fetch
[IO] GET /pengguna/1 dimulai
[IO] GET /pengguna/1 selesai
[Main] setelah fetch
onResult: Budi-1
[Main] sebelum fetch
[IO] GET /pengguna/2 dimulai
[IO] GET /pengguna/2 selesai
[Main] setelah fetch
onResult: Budi-2
[Main] sebelum fetch
[IO] GET /pengguna/3 dimulai
[IO] GET /pengguna/3 selesai
[Main] setelah fetch
onResult: Budi-3
Hasil dikumpulkan: [Budi-1, Budi-2, Budi-3]
=== Demonstrasi dispatcher IO ===
[IO] berjalan di thread=DefaultDispatcher-worker-1
SelesaiPerhatikan urutan output: [Main] sebelum fetch → [IO] GET ... dimulai → [IO] GET ... selesai → [Main] setelah fetch. Coroutine berpindah dispatcher ketika memanggil suspend function yang memakai withContext(Dispatchers.IO). Setelah selesai, eksekusi kembali ke dispatcher asal (Main/Default).
Referensi Lanjutan #
- Guide to app architecture — panduan arsitektur Android yang merekomendasikan coroutines untuk operasi asinkron di lapisan UI dan data
- Coroutines overview
— rujukan Kotlin coroutines sebagai dasar pemakaian
lifecycleScopedanviewModelScope
Selanjutnya #
Coroutines mengurus asinkron, namun dari mana data berasal? Bab berikutnya membahas Repository Pattern — abstraksi yang menyatukan sumber network, cache, dan database menjadi satu titik akses bagi ViewModel.