Bergabunglah bersama kami di ⁠#Android11: The Beta Launch Show pada tanggal 3 Juni!

Menyempurnakan performa aplikasi dengan coroutine Kotlin

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 membantu menyelesaikan dua masalah utama:

  • Mengelola tugas yang berjalan lama dan mungkin memblokir thread utama dan menyebabkan aplikasi berhenti berfungsi.
  • Menyediakan main-safety, atau memanggil jaringan atau operasi disk yang aman dari thread utama.

Topik ini menjelaskan bagaimana Anda dapat menggunakan coroutine Kotlin untuk mengatasi masalah ini sehingga Anda dapat menulis kode aplikasi yang lebih rapi dan ringkas.

Mengelola tugas yang berjalan lama

Di Android, setiap aplikasi memiliki thread utama yang menangani antarmuka pengguna dan mengelola interaksi pengguna. Jika aplikasi Anda menugaskan terlalu banyak pekerjaan ke thread utama, aplikasi dapat tampak berhenti berfungsi atau melambat secara signifikan. Permintaan jaringan, penguraian JSON, pembacaan atau penulisan dari database, atau bahkan hanya melakukan iterasi terhadap daftar besar dapat menyebabkan aplikasi Anda berjalan cukup lambat dan menyebabkan terlihatnya jank—UI yang mengalami freeze atau melambat, sehingga respons terhadap peristiwa sentuh sangat lama. Operasi yang berjalan lama ini harus berjalan di luar thread utama.

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

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) { /* ... */ }

Coroutine membuat fungsi reguler dengan menambahkan dua operasi untuk menangani tugas yang berjalan lama. Selain invoke (atau call) dan return, coroutine juga 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.

Pada contoh di atas, get() masih berjalan di 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.

Menentukan CoroutineScope

Saat menentukan coroutine, Anda juga harus menentukan CoroutineScope-nya. CoroutineScope mengelola satu atau beberapa coroutine terkait. Anda juga dapat menggunakan CoroutineScope untuk memulai coroutine baru dalam cakupan tersebut. Namun, tidak seperti dispatcher, CoroutineScope tidak menjalankan coroutine.

Salah satu fungsi penting CoroutineScope adalah menghentikan eksekusi coroutine saat pengguna meninggalkan area konten dalam aplikasi Anda. Dengan CoroutineScope, Anda dapat memastikan bahwa semua operasi yang berjalan berhenti dengan benar.

Menggunakan CoroutineScope dengan komponen Arsitektur Android

Di Android, Anda dapat mengaitkan penerapan CoroutineScope dengan siklus komponen. Hal ini memungkinkan Anda menghindari kebocoran memori atau melakukan pekerjaan tambahan untuk aktivitas atau fragmen yang tidak lagi relevan bagi pengguna. Menggunakan komponen Jetpack, komponen tersebut muat secara alami dalam ViewModel. Karena suatu ViewModel tidak dimusnahkan selama perubahan konfigurasi (seperti rotasi layar), Anda tidak perlu khawatir tentang pembatalan atau pengaktifan ulang coroutine Anda.

Cakupan mengetahui tentang setiap coroutine yang dimulainya. Artinya, Anda dapat membatalkan semua yang telah dimulai dalam cakupan kapan pun. Cakupan akan menyebar sendiri, jadi jika coroutine memulai coroutine lain, kedua coroutine akan memiliki cakupan yang sama. Artinya, meskipun library lain memulai coroutine dari cakupan, Anda dapat membatalkannya kapan pun. Hal ini terutama penting jika Anda menjalankan coroutine dalam ViewModel. Jika ViewModel Anda dimusnahkan karena pengguna telah meninggalkan layar, semua pekerjaan asinkron yang sedang dikerjakan harus dihentikan. Jika tidak, Anda akan menyia-nyiakan resource dan berpotensi mengalami kebocoran memori. Jika Anda memiliki pekerjaan asinkron yang harus terus berlanjut setelah Anda memusnahkan ViewModel, pekerjaan tersebut harus dilakukan di lapisan bawah arsitektur aplikasi Anda.

Dengan Library KTX untuk komponen Arsitektur Android, Anda juga dapat menggunakan properti ekstensi, viewModelScope, untuk membuat coroutine yang dapat dijalankan hingga ViewModel dimusnahkan.

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 result 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.

Berdasarkan contoh sebelumnya, berikut adalah coroutine dengan properti ekstensi KTX viewModelScope yang menggunakan launch untuk beralih dari fungsi biasa ke coroutine:

fun onDocsNeeded() {
    viewModelScope.launch {    // Dispatchers.Main
        fetchDocs()            // Dispatchers.Main (suspend function call)
    }
}

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.

Komponen arsitektur dengan dukungan bawaan

Beberapa komponen Arsitektur, termasuk ViewModel dan Lifecycle, menyertakan dukungan bawaan untuk coroutine melalui anggota CoroutineScope miliknya sendiri.

Misalnya, ViewModel menyertakan viewModelScope bawaan. Hal ini memberikan cara standar untuk meluncurkan coroutine dalam cakupan ViewModel, seperti yang ditunjukkan pada contoh berikut:

class MyViewModel : ViewModel() {

    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }

    /**
    * Heavy operation that cannot be done in the Main Thread
    */
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

LiveData juga menggunakan coroutine dengan blok liveData:

liveData {
    // runs in its own LiveData-specific scope
}

Untuk informasi lebih lanjut tentang komponen Arsitektur dengan dukungan coroutine bawaan, lihat Menggunakan coroutine Kotlin dengan komponen Arsitektur.

Informasi selengkapnya

Untuk informasi selengkapnya terkait coroutine, lihat link berikut: