Back to IF3210 Pengembangan Aplikasi Piranti Bergerak

Coroutines for Asynchronous Programming

Questions/Cues

  • Mengapa operasi long-running tidak boleh dijalankan di main thread?
  • Apa keunggulan Kotlin Coroutines dibanding threading tradisional?
  • Apa perbedaan antara Dispatchers.Main, IO, dan Default?
  • Bagaimana viewModelScope membantu mengelola lifecycle coroutine?
  • Apa perbedaan antara launch dan async dalam coroutine?

Reference Points

  • IF3210 Pengembangan Aplikasi Piranti Bergerak (Slides 29-45)

Mengapa Pemrograman Asinkron Diperlukan

Setiap aplikasi Android berjalan di atas main thread (juga dikenal sebagai UI thread). Thread ini bertanggung jawab untuk menggambar UI dan merespons interaksi pengguna. Jika main thread diblokir oleh operasi yang memakan waktu lama (misalnya query database, request jaringan, atau pemrosesan file besar), aplikasi akan berhenti merespons dan sistem Android akan menampilkan dialog “Application Not Responding” (ANR) setelah 5 detik.

Operasi yang dikategorikan sebagai long-running tasks dan harus dijalankan di luar main thread:

  • Permintaan jaringan (HTTP/REST API calls)
  • Operasi baca/tulis database
  • Pemrosesan file besar (gambar, video, CSV)
  • Komputasi intensif

Pendekatan tradisional untuk menangani ini adalah menggunakan Thread Java secara manual atau callback, namun keduanya memiliki kekurangan: Thread manual sulit dikelola dan callback menciptakan “callback hell” (nested callback yang sulit dibaca dan di-debug).

Kotlin Coroutines

Kotlin Coroutines adalah solusi yang direkomendasikan Google untuk pemrograman asinkron di Android. Coroutine adalah blok kode yang dapat di-suspend dan di-resume tanpa memblokir thread yang menjalankannya. Keunggulan utama coroutine dibanding pendekatan lain:

  • Lightweight: Ribuan coroutine dapat berjalan bersamaan tanpa overhead yang signifikan, karena coroutine bukan thread — mereka dijalankan di atas thread pool yang jauh lebih kecil
  • Fewer memory leaks: Coroutines mendukung structured concurrency — coroutine anak selalu selesai sebelum coroutine induknya, mengurangi risiko memory leak
  • Built-in cancellation support: Pembatalan dapat dipropagasi secara otomatis ke seluruh hierarki coroutine
  • Kode lebih bersih: Kode asinkron ditulis seperti kode sekuensial biasa tanpa callback berlapis

Suspend Functions

Fungsi yang dapat di-suspend ditandai dengan kata kunci suspend. Sebuah suspend function dapat menangguhkan eksekusi coroutine yang memanggilnya tanpa memblokir thread, lalu melanjutkannya kembali saat hasilnya siap:

suspend fun fetchUserData(): User {
    return withContext(Dispatchers.IO) {
        // Operasi jaringan — tidak memblokir main thread
        apiService.getUser()
    }
}

Suspend function hanya dapat dipanggil dari dalam coroutine atau suspend function lain.

Coroutine Dispatchers

Dispatcher menentukan thread atau thread pool mana yang akan digunakan coroutine untuk menjalankan kodenya:

flowchart LR
    D["Coroutine Dispatchers"]
    MAIN["Dispatchers.Main\nUI thread\nUpdate UI, interaksi LiveData"]
    IO["Dispatchers.IO\nThread pool I/O\nDatabase, jaringan, file"]
    DEF["Dispatchers.Default\nThread pool CPU\nSorting, parsing JSON"]
    D --> MAIN
    D --> IO
    D --> DEF
  • Dispatchers.Main: Menjalankan coroutine di main thread. Digunakan untuk operasi yang menyentuh UI, seperti mengupdate LiveData atau memanipulasi View
  • Dispatchers.IO: Dioptimalkan untuk operasi I/O yang banyak menunggu (blocking I/O). Menggunakan thread pool dengan jumlah thread yang besar. Cocok untuk operasi database Room dan HTTP request
  • Dispatchers.Default: Dioptimalkan untuk pekerjaan intensif CPU. Menggunakan thread pool dengan jumlah thread sebanyak jumlah core CPU. Cocok untuk pengurutan list besar atau parsing data

withContext untuk Perpindahan Dispatcher

Fungsi withContext(dispatcher) digunakan untuk menjalankan blok kode tertentu pada dispatcher yang berbeda, lalu secara otomatis kembali ke dispatcher semula setelah blok selesai:

suspend fun insertData(word: Word) {
    withContext(Dispatchers.IO) {
        // Blok ini berjalan di IO thread pool
        wordDao.insert(word)
    }
    // Kembali ke dispatcher semula setelah blok selesai
}

Pola ini sangat umum: coroutine dimulai di Main, berpindah ke IO untuk operasi database/jaringan, lalu hasil kembali diproses di Main untuk dirender ke UI.

CoroutineScope dan viewModelScope

CoroutineScope mendefinisikan batas hidup sekumpulan coroutine. Saat scope dibatalkan, semua coroutine di dalamnya otomatis dibatalkan juga. Beberapa scope yang tersedia:

  • GlobalScope: Coroutine hidup selama aplikasi berjalan. Umumnya tidak direkomendasikan karena dapat menyebabkan memory leak
  • viewModelScope: Scope yang terikat ke lifecycle ViewModel. Coroutine otomatis dibatalkan saat ViewModel di-clear (Activity/Fragment destroyed). Ini adalah scope yang paling direkomendasikan untuk operasi yang dipicu dari UI
  • lifecycleScope: Scope yang terikat ke lifecycle Activity/Fragment. Otomatis dibatalkan saat owner destroyed

Contoh penggunaan viewModelScope dengan Room:

class WordViewModel(private val repository: WordRepository) : ViewModel() {
 
    val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
 
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

launch vs async

Terdapat dua builder utama untuk memulai coroutine baru dalam sebuah scope:

  • launch: Memulai coroutine yang menjalankan pekerjaan tanpa mengembalikan hasil. Mengembalikan objek Job yang dapat digunakan untuk membatalkan coroutine. Cocok untuk operasi fire-and-forget seperti insert ke database
  • async: Memulai coroutine yang mengembalikan hasil. Mengembalikan objek Deferred<T> yang hasilnya diambil dengan memanggil .await(). Cocok saat dua operasi perlu dijalankan secara paralel dan hasilnya digabungkan

Summary

Kotlin Coroutines mengatasi masalah pemblokiran main thread (yang menyebabkan ANR) dengan menyediakan blok kode yang dapat di-suspend dan di-resume secara efisien. Tiga Dispatcher utama mengontrol thread eksekusi: Main untuk UI, IO untuk operasi database dan jaringan, dan Default untuk komputasi CPU. Fungsi withContext() digunakan untuk berpindah dispatcher di tengah coroutine. viewModelScope adalah scope yang direkomendasikan karena terikat ke lifecycle ViewModel dan otomatis membatalkan semua coroutine saat ViewModel destroyed. Builder launch digunakan untuk operasi tanpa nilai kembalian, sementara async digunakan untuk operasi paralel yang menghasilkan nilai via .await().