Coroutine Kotlin di Android

Coroutine adalah pola desain serentak yang dapat Anda gunakan di Android untuk menyederhanakan kode yang dieksekusi secara asinkron. Coroutine ditambahkan pada Kotlin dalam versi 1.3 dan didasarkan pada konsep yang telah ditetapkan dari bahasa lain.

Di Android, coroutine berguna untuk mengelola tugas yang berjalan lama dan mungkin memblokir thread utama serta menyebabkan aplikasi tidak responsif. Lebih dari 50% developer profesional yang menggunakan coroutine telah melaporkan peningkatan produktivitas. Topik ini menjelaskan bagaimana Anda dapat menggunakan coroutine Kotlin untuk mengatasi masalah ini, sehingga Anda dapat menulis kode aplikasi yang lebih rapi dan ringkas.

Fitur

Coroutine adalah solusi yang direkomendasikan untuk pemrograman asinkron di Android. Fitur penting meliputi:

  • Ringan: Anda dapat menjalankan banyak coroutine pada satu thread karena adanya dukungan untuk penangguhan, yang tidak memblokir thread tempat coroutine berjalan. Penangguhan menghemat memori melalui pemblokiran sekaligus mendukung banyak operasi serentak.
  • Lebih sedikit kebocoran memori: Gunakan konkurensi terstruktur untuk menjalankan operasi dalam cakupan.
  • Dukungan pembatalan bawaan: Pembatalan otomatis disebarkan melalui hierarki coroutine yang sedang berjalan.
  • Integrasi Jetpack: Banyak library Jetpack dilengkapi ekstensi yang menyediakan dukungan penuh coroutine. Beberapa library juga menyediakan cakupan coroutine sendiri yang dapat Anda gunakan untuk membuat struktur serentak.

Ringkasan contoh

Berdasarkan Panduan arsitektur aplikasi, contoh dalam topik ini membuat permintaan jaringan dan menampilkan hasilnya ke thread utama, tempat aplikasi kemudian dapat menampilkan hasilnya kepada pengguna.

Secara khusus, komponen Arsitektur ViewModel memanggil lapisan repositori pada thread utama untuk memicu permintaan jaringan. Panduan ini akan melakukan iterasi melalui berbagai solusi yang menggunakan coroutine untuk mencegah thread utama terblokir.

ViewModel menyertakan serangkaian ekstensi KTX yang berfungsi langsung dengan coroutine. Ekstensi tersebut adalah library lifecycle-viewmodel-ktx dan digunakan dalam panduan ini.

Info dependensi

Untuk menggunakan coroutine dalam project Android, tambahkan dependensi berikut ke file build.gradle aplikasi Anda:

Groovy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Menjalankan eksekusi pada thread latar belakang

Pembuatan permintaan jaringan pada thread utama akan menyebabkannya menunggu, atau memblokir, hingga menerima respons. Karena thread diblokir, OS tidak dapat memanggil onDraw(), yang menyebabkan aplikasi berhenti berfungsi dan berpotensi memunculkan dialog Aplikasi Tidak Merespons (ANR). Untuk pengalaman pengguna yang lebih baik, jalankan operasi ini di thread latar belakang.

Pertama, mari kita lihat class Repository dan lihat caranya membuat permintaan jaringan:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest bersifat sinkron dan memblokir thread pemanggil. Untuk mencontohkan respons permintaan jaringan, kami memiliki class Result sendiri.

ViewModel memicu permintaan jaringan saat pengguna mengklik, misalnya, sebuah tombol:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Dengan kode sebelumnya, LoginViewModel memblokir UI thread saat membuat permintaan jaringan. Solusi yang paling sederhana untuk mengeluarkan eksekusi dari thread utama adalah membuat coroutine baru dan menjalankan permintaan jaringan pada thread I/O:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Mari kita pelajari kode coroutine dalam fungsi login:

  • viewModelScope adalah CoroutineScope yang telah ditetapkan dan disertakan dengan ekstensi KTX ViewModel. Perlu diingat bahwa semua coroutine harus dijalankan dalam cakupan. CoroutineScope mengelola satu atau beberapa coroutine terkait.
  • launch adalah fungsi yang membuat coroutine dan mengirimkan eksekusi isi fungsinya ke dispatcher yang sesuai.
  • Dispatchers.IO menunjukkan bahwa coroutine ini harus dieksekusi pada thread yang telah disiapkan untuk operasi I/O.

Fungsi login dieksekusi sebagai berikut:

  • Aplikasi memanggil fungsi login dari lapisan View pada thread utama.
  • launch membuat coroutine baru, dan permintaan jaringan dibuat secara terpisah pada thread yang telah disiapkan untuk operasi I/O.
  • Saat coroutine berjalan, fungsi login akan melanjutkan eksekusi dan kembali, mungkin sebelum permintaan jaringan selesai. Perlu diketahui bahwa respons jaringan diabaikan untuk saat ini agar lebih praktis.

Karena dimulai dengan viewModelScope, coroutine ini dieksekusi dalam cakupan ViewModel. Jika ViewModel dihapus karena pengguna keluar dari layar, viewModelScope akan dibatalkan secara otomatis, dan semua coroutine yang berjalan juga akan dibatalkan.

Satu masalah dengan contoh sebelumnya adalah bahwa apa pun yang memanggil makeLoginRequest harus ingat untuk memindahkan eksekusi dari thread utama secara eksplisit. Mari kita lihat cara mengubah Repository untuk menyelesaikan masalah ini.

Menggunakan coroutine untuk main-safety

Kami menganggap sebuah fungsi bersifat main-safe jika tidak memblokir update UI pada thread utama. Fungsi makeLoginRequest tidak main-safe, karena memanggil makeLoginRequest dari thread utama akan memblokir UI. Gunakan fungsi withContext() dari library coroutine untuk memindahkan eksekusi coroutine ke thread lain:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) memindahkan eksekusi coroutine ke thread I/O sehingga fungsi panggilan kami menjadi main-safe dan memungkinkan UI diupdate sesuai kebutuhan.

makeLoginRequest juga ditandai dengan kata kunci suspend. Kata kunci ini adalah cara Kotlin menerapkan fungsi yang akan dipanggil dari dalam coroutine.

Dalam contoh berikut, coroutine dibuat di LoginViewModel. Saat makeLoginRequest mengeluarkan eksekusi dari thread utama, coroutine dalam fungsi login kini dapat dieksekusi pada thread utama:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Perlu diingat bahwa coroutine masih diperlukan di sini, karena makeLoginRequest adalah fungsi suspend, dan semua fungsi suspend harus dieksekusi dalam coroutine.

Dalam beberapa hal, kode ini berbeda dengan contoh login sebelumnya:

  • launch tidak menggunakan parameter Dispatchers.IO. Jika Dispatcher tidak diteruskan ke launch, setiap coroutine yang diluncurkan dari viewModelScope akan dijalankan di thread utama.
  • Kini hasil permintaan jaringan ditangani untuk menampilkan UI yang berhasil atau gagal.

Kini fungsi login berjalan seperti berikut:

  • Aplikasi memanggil fungsi login() dari lapisan View pada thread utama.
  • launch membuat coroutine baru pada thread utama, dan coroutine akan memulai eksekusi.
  • Dalam coroutine, panggilan ke loginRepository.makeLoginRequest() kini menangguhkan eksekusi coroutine lebih lanjut hingga blok withContext dalam makeLoginRequest() selesai berjalan.
  • Setelah blok withContext selesai, coroutine pada login() akan melanjutkan eksekusi pada thread utama dengan hasil permintaan jaringan.

Menangani pengecualian

Untuk menangani pengecualian yang dapat ditampilkan oleh lapisan Repository, gunakan dukungan pengecualian bawaan Kotlin. Dalam contoh berikut, kami menggunakan blok try-catch:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Dalam contoh ini, setiap pengecualian tidak terduga yang ditampilkan oleh panggilan makeLoginRequest() ditangani sebagai error dalam UI.

Referensi coroutine lainnya

Untuk penjelasan coroutine di Android lebih lanjut, lihat Meningkatkan performa aplikasi dengan coroutine Kotlin.

Untuk referensi coroutine lainnya, lihat link berikut: