Menyempurnakan performa aplikasi dengan coroutine Kotlin

Dengan Coroutine Kotlin, Anda dapat menulis kode asinkron yang sederhana dan jelas, yang menjaga aplikasi tetap responsif saat mengelola tugas yang berjalan lama seperti panggilan jaringan atau operasi disk.

Topik ini memberikan informasi mendetail tentang coroutine di Android. Jika belum memahami coroutine, pastikan Anda membaca coroutine Kotlin di Android sebelum membaca topik ini.

Mengelola tugas yang berjalan lama

Coroutine membuat fungsi reguler dengan menambahkan dua operasi untuk menangani tugas yang berjalan lama. Selain invoke (atau call) dan return, coroutine menambahkan suspend dan resume:

  • suspend menjeda eksekusi coroutine saat ini yang menyimpan semua variabel lokal.
  • resume melanjutkan eksekusi coroutine yang ditangguhkan dari titik penangguhannya.

Anda dapat memanggil fungsi suspend hanya dari fungsi suspend lainnya atau dengan menggunakan builder coroutine seperti launch untuk memulai coroutine baru.

Contoh berikut menunjukkan implementasi coroutine sederhana untuk tugas hipotesis yang berjalan lama:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

Dalam contoh ini, get() masih berjalan pada thread utama, tetapi menangguhkan coroutine sebelum memulai permintaan jaringan. Saat permintaan jaringan selesai, get akan melanjutkan coroutine yang ditangguhkan, bukan menggunakan callback untuk memberi tahu thread utama.

Kotlin menggunakan frame stack untuk mengelola fungsi mana yang berjalan bersama dengan variabel lokal apa pun. Saat menangguhkan coroutine, frame stack saat ini akan disalin dan disimpan untuk nanti. Saat melanjutkan, frame stack akan disalin kembali dari tempatnya disimpan, dan fungsi mulai berjalan kembali. Meskipun kode mungkin terlihat seperti permintaan pemblokiran berurutan biasa, coroutine memastikan agar permintaan jaringan menghindari pemblokiran thread utama.

Menggunakan coroutine untuk main-safety

Coroutine Kotlin menggunakan dispatcher untuk menentukan thread yang digunakan untuk eksekusi coroutine. Untuk menjalankan kode di luar thread utama, Anda dapat memberi tahu coroutine Kotlin untuk melakukan pekerjaan pada dispatcher Default atau IO. Di Kotlin, semua coroutine harus dijalankan dalam dispatcher, meskipun sedang berjalan di thread utama. Coroutine dapat menangguhkan dirinya sendiri, dan dispatcher bertanggung jawab untuk melanjutkannya.

Untuk menentukan tempat menjalankan coroutine, Kotlin menyediakan tiga dispatcher yang dapat Anda gunakan:

  • Dispatchers.Main - Gunakan dispatcher ini untuk menjalankan coroutine pada thread utama Android. Dispatcher ini harus digunakan hanya untuk berinteraksi dengan UI dan melakukan pekerjaan cepat. Contohnya mencakup pemanggilan fungsi suspend, menjalankan operasi framework UI Android, dan mengupdate objek LiveData.
  • Dispatchers.IO - Dispatcher ini dioptimalkan untuk menjalankan disk atau I/O jaringan di luar thread utama. Contohnya termasuk menggunakan Komponen room, membaca dari atau menulis ke file, dan menjalankan operasi jaringan apa pun.
  • Dispatchers.Default - Dispatcher ini dioptimalkan untuk melakukan pekerjaan yang membebani CPU di luar thread utama. Contoh kasus penggunaan mencakup pengurutan daftar dan penguraian JSON.

Melanjutkan contoh sebelumnya, Anda dapat menggunakan dispatcher untuk menentukan ulang fungsi get. Di dalam isi get, panggil withContext(Dispatchers.IO) untuk membuat blok yang berjalan di pool thread IO. Setiap kode yang Anda masukkan ke dalam blok tersebut selalu dijalankan melalui dispatcher IO. Karena withContext itu sendiri memegang fungsi penangguhan, fungsi get juga merupakan fungsi penangguhan.

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

Dengan coroutine, Anda dapat mengirim thread dengan kontrol yang sangat baik. Karena withContext() memungkinkan Anda mengontrol kumpulan thread dari setiap baris kode tanpa memasukkan callback, Anda dapat menerapkannya pada fungsi yang sangat kecil seperti membaca dari database atau melakukan permintaan jaringan. Praktik yang baik adalah menggunakan withContext() untuk memastikan setiap fungsi berada dalam main-safe, yang berarti Anda dapat memanggil fungsi tersebut dari thread utama. Dengan cara ini, pemanggil tidak perlu memikirkan thread mana yang harus digunakan untuk menjalankan fungsi.

Pada contoh sebelumnya, fetchDocs() mengeksekusi di thread utama; tetapi dapat memanggil get dengan aman, yang melakukan permintaan jaringan di latar belakang. Karena coroutine mendukung suspend dan resume, coroutine pada thread utama dilanjutkan dengan hasil get segera setelah blok withContext selesai.

Performa withContext()

withContext() tidak menambahkan biaya tambahan dibandingkan dengan implementasi berbasis callback yang setara. Selain itu, Anda juga dapat mengoptimalkan panggilan withContext() melampaui implementasi berbasis callback yang setara dalam beberapa situasi. Misalnya, jika suatu fungsi melakukan sepuluh panggilan ke jaringan, Anda dapat memberi tahu Kotlin untuk hanya beralih thread sekali menggunakan withContext() luar. Kemudian, meskipun library jaringan menggunakan withContext() beberapa kali, library tetap berada di dispatcher yang sama dan menghindari pengalihan thread. Selain itu, Kotlin juga mengoptimalkan pengalihan antara Dispatchers.Default dan Dispatchers.IO untuk menghindari pengalihan thread jika memungkinkan.

Memulai coroutine

Anda dapat memulai coroutine dengan salah satu dari dua cara berikut:

  • launch memulai coroutine baru dan tidak akan menampilkan hasilnya ke pemanggil. Setiap pekerjaan yang dianggap "aktif dan dilupakan" dapat dimulai menggunakan launch.
  • async memulai coroutine baru dan memungkinkan Anda menampilkan hasil dengan fungsi penangguhan yang disebut await.

Biasanya, Anda harus melakukan launch coroutine baru dari fungsi biasa karena fungsi biasa tidak dapat memanggil await. Gunakan async hanya saat berada di dalam coroutine lain atau saat berada di dalam fungsi penangguhan dan melakukan dekomposisi paralel.

Dekomposisi paralel

Semua coroutine yang dimulai dengan fungsi suspend harus dihentikan ketika fungsi tersebut kembali, sehingga Anda mungkin perlu memastikan agar coroutine tersebut selesai sebelum kembali. Dengan pengoperasian serentak terstruktur di Kotlin, Anda dapat menentukan coroutineScope yang memulai satu atau beberapa coroutine. Selain itu, dengan menggunakan await() (untuk coroutine tunggal) atau awaitAll() (untuk beberapa coroutine), Anda dapat menjamin agar coroutine selesai sebelum kembali dari fungsi tersebut.

Sebagai contoh, mari tentukan coroutineScope yang mengambil dua dokumen secara asinkron. Dengan memanggil await() pada setiap referensi yang ditangguhkan, kami menjamin bahwa kedua operasi async itu akan selesai sebelum menampilkan nilai:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

Anda juga dapat menggunakan awaitAll() pada koleksi, seperti yang ditunjukkan pada contoh berikut:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

Meskipun fetchTwoDocs() meluncurkan coroutine baru dengan async, fungsi ini menggunakan awaitAll() untuk menunggu peluncuran coroutine agar selesai sebelum kembali. Namun, perhatikan bahwa meskipun kami tidak memanggil awaitAll(), builder coroutineScope tidak melanjutkan coroutine yang memanggil fetchTwoDocs hingga semua proses coroutine baru selesai.

Selain itu, coroutineScope menangkap setiap pengecualian yang dikeluarkan oleh coroutine dan mengarahkannya kembali ke pemanggil.

Untuk informasi selengkapnya tentang dekomposisi paralel, lihat Menuliskan fungsi penangguhan.

Konsep coroutine

CoroutineScope

CoroutineScope memantau setiap coroutine yang dibuat olehnya menggunakan launch atau async. Pekerjaan yang sedang berlangsung (misalnya, coroutine yang berjalan) dapat dibatalkan dengan memanggil scope.cancel() kapan saja. Di Android, beberapa library KTX menyediakan CoroutineScope sendiri untuk class siklus proses tertentu. Misalnya, ViewModel memiliki viewModelScope, dan Lifecycle memiliki lifecycleScope. Namun, tidak seperti dispatcher, CoroutineScope tidak menjalankan coroutine.

viewModelScope juga digunakan dalam contoh yang ditemukan di Mengelola thread latar belakang di Android dengan Coroutine. Namun, jika perlu membuat CoroutineScope sendiri untuk mengontrol siklus proses coroutine pada lapisan aplikasi tertentu, Anda dapat membuatnya sebagai berikut:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

Cakupan yang dibatalkan tidak dapat membuat lebih banyak coroutine. Oleh karena itu, sebaiknya hanya panggil scope.cancel() ketika class yang mengontrol siklus prosesnya sedang dihapus. Saat menggunakan viewModelScope, class ViewModel akan membatalkan cakupan secara otomatis di metode onCleared() ViewModel.

Tugas

Job adalah tuas untuk coroutine. Setiap coroutine yang Anda buat dengan launch atau async akan menampilkan instance Job yang mengidentifikasi coroutine secara unik dan mengelola siklus prosesnya. Anda juga dapat meneruskan Job ke CoroutineScope untuk mengelola siklus prosesnya lebih lanjut, seperti pada contoh berikut:

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext menentukan perilaku coroutine menggunakan serangkaian elemen berikut:

Untuk coroutine baru yang dibuat dalam cakupan, instance Job baru akan ditetapkan ke coroutine baru, dan elemen CoroutineContext lainnya diwariskan dari cakupan yang menampungnya. Anda dapat mengganti elemen yang diwariskan dengan meneruskan CoroutineContext baru ke fungsi launch atau async. Perlu diketahui bahwa meneruskan Job ke launch atau async tidak akan berpengaruh, karena instance baru Job selalu ditetapkan ke coroutine baru.

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + "BackgroundCoroutine") {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

Referensi coroutine lainnya

Untuk mempelajari coroutine lebih lanjut, lihat referensi tambahan berikut:

Dokumentasi

Codelab

Video

Postingan blog