Coroutine Kotlin memungkinkan Anda untuk menulis kode asinkron yang bersih dan disederhanakan yang menjaga aplikasi Anda tetap responsif sembari mengelola tugas yang berjalan lama seperti panggilan jaringan atau operasi {i>disk<i}.
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 objekLiveData
. - 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 memberikan overhead ekstra dibandingkan dengan berbasis callback yang setara
terlepas dari implementasi layanan. 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 menampilkan hasilnya ke pemanggil. Setiap pekerjaan yang dianggap "aktif dan dilupakan" dapat dimulai menggunakanlaunch
.async
memulai coroutine baru dan memungkinkan Anda menampilkan hasil dengan penangguhan fungsi yang disebutawait
.
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 dalam 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
melacak setiap coroutine yang dibuatnya 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 handle 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:
Job
: Mengontrol siklus proses coroutine.CoroutineDispatcher
: Mengirimkan pekerjaan ke thread yang sesuai.CoroutineName
: Nama coroutine, berguna untuk proses debug.CoroutineExceptionHandler
: Menangani pengecualian yang tidak terdeteksi.
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 + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Referensi coroutine lainnya
Untuk referensi coroutine lainnya, lihat link berikut: