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.

kotlin viewModelScope.launch untuk load data
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.

kotlin Pindah ke IO untuk network
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.

kotlin Retrofit suspend interface
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:

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

text
=== 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
Selesai

Perhatikan 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 lifecycleScope dan viewModelScope

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.