Dalam codelab ini, Anda akan mempelajari cara menggunakan Coroutine Kotlin di aplikasi Android—cara baru mengelola thread latar belakang yang dapat menyederhanakan kode dengan mengurangi kebutuhan callback. Coroutine adalah fitur Kotlin yang mengonversi callback asinkron untuk tugas yang berjalan lama, seperti akses jaringan atau database, menjadi kode berurutan.
Berikut cuplikan kode untuk memberikan ide tentang apa yang akan Anda lakukan.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Kode berbasis callback akan dikonversi ke kode berurutan menggunakan coroutine.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Anda akan memulai dengan aplikasi yang ada, dibuat menggunakan Komponen Arsitektur, yang menggunakan gaya callback untuk tugas yang berjalan lama.
Di akhir codelab ini, Anda akan memiliki pengalaman yang cukup untuk menggunakan coroutine di aplikasi untuk memuat data dari jaringan, dan Anda akan dapat mengintegrasikan coroutine ke dalam aplikasi. Anda juga akan memahami praktik terbaik untuk coroutine, dan cara menulis pengujian terhadap kode yang menggunakan coroutine.
Prasyarat
- Pemahaman tentang Komponen Arsitektur
ViewModel
,LiveData
,Repository
, danRoom
. - Pengalaman dengan sintaks Kotlin, termasuk fungsi ekstensi dan lambda.
- Pemahaman dasar tentang menggunakan thread pada Android, termasuk thread utama, thread latar belakang, dan callback.
Yang akan Anda lakukan
- Memanggil kode yang ditulis dengan coroutine dan memperoleh hasil.
- Gunakan fungsi penangguhan untuk membuat kode asinkron berurutan.
- Gunakan
launch
danrunBlocking
untuk mengontrol cara kode dieksekusi. - Pelajari teknik untuk mengonversi API yang ada ke coroutine menggunakan
suspendCoroutine
. - Menggunakan coroutine dengan Komponen Arsitektur.
- Pelajari praktik terbaik untuk menguji coroutine.
Yang Anda butuhkan
- Android Studio 3.5 (codelab mungkin berfungsi dengan versi lain, tetapi beberapa hal mungkin hilang atau terlihat berbeda).
Jika Anda mengalami masalah (bug kode, kesalahan gramatikal, susunan kata yang tidak jelas, dll.) saat mengerjakan codelab ini, laporkan masalah tersebut melalui link Laporkan kesalahan di pojok kiri bawah codelab.
Mendownload kode
Klik link berikut guna mendownload semua kode untuk codelab ini:
... atau clone repositori GitHub dari command line dengan menggunakan perintah berikut:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Pertanyaan yang sering diajukan
Pertama-tama, mari kita lihat bagaimana tampilan aplikasi contoh. Ikuti petunjuk ini untuk membuka aplikasi contoh di Android Studio.
- Jika Anda mendownload file zip
kotlin-coroutines
, ekstrak zip file tersebut. - Buka project
coroutines-codelab
di Android Studio. - Pilih modul aplikasi
start
. - Klik tombol
Run, lalu pilih emulator atau hubungkan perangkat Android Anda, yang harus mampu menjalankan Android Lollipop (SDK minimum yang didukung adalah 21). Layar Coroutine Kotlin akan muncul:
Aplikasi awal ini menggunakan thread untuk meningkatkan penundaan dihitung dalam jumlah singkat setelah Anda menekan layar. Tindakan ini juga akan mengambil judul baru dari jaringan dan menampilkannya di layar. Cobalah sekarang, dan Anda akan melihat jumlah dan pesan berubah setelah beberapa saat. Dalam codelab ini, Anda akan mengonversi aplikasi ini untuk menggunakan coroutine.
Aplikasi ini menggunakan Komponen Arsitektur untuk memisahkan kode UI di MainActivity
dari logika aplikasi di MainViewModel
. Luangkan waktu untuk membiasakan diri dengan struktur project.
MainActivity
menampilkan UI, mendaftarkan pemroses klik, dan dapat menampilkanSnackbar
. Ini akan meneruskan peristiwa keMainViewModel
dan mengupdate layar berdasarkanLiveData
diMainViewModel
.MainViewModel
menangani peristiwa dionMainViewClicked
dan akan berkomunikasi denganMainActivity
menggunakanLiveData.
Executors
menentukanBACKGROUND,
yang dapat menjalankan berbagai hal di thread latar belakang.TitleRepository
mengambil hasil dari jaringan dan menyimpannya ke database.
Menambahkan coroutine ke project
Untuk menggunakan coroutine di Kotlin, Anda harus menyertakan library coroutines-core
dalam file build.gradle (Module: app)
project Anda. Project codelab sudah melakukannya untuk Anda, jadi Anda tidak perlu melakukannya untuk menyelesaikan codelab.
Coroutine di Android tersedia sebagai library inti, dan ekstensi khusus Android:
- kotlinx-corountines-core — Antarmuka utama untuk menggunakan coroutine di Kotlin
- kotlinx-coroutines-android — Dukungan untuk thread Utama Android di coroutine
Aplikasi awal sudah menyertakan dependensi dalam build.gradle.
Saat membuat project aplikasi baru, Anda harus membuka build.gradle (Module: app)
dan menambahkan dependensi coroutine ke project.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Di Android, penting untuk menghindari pemblokiran thread utama. Thread utama adalah satu thread yang menangani semua update pada UI. Ini juga merupakan thread yang memanggil semua pengendali klik dan callback UI lainnya. Oleh karena itu, aplikasi harus berjalan lancar untuk menjamin pengalaman pengguna yang luar biasa.
Agar aplikasi Anda ditampilkan kepada pengguna tanpa jeda yang terlihat, thread utama harus memperbarui layar setiap 16 md atau lebih, yaitu sekitar 60 frame per detik. Banyak tugas umum yang memerlukan waktu lebih lama, seperti menguraikan set data JSON yang besar, menulis data ke database, atau mengambil data dari jaringan. Oleh karena itu, memanggil kode seperti ini dari thread utama dapat menyebabkan aplikasi dijeda, tersendat, atau bahkan berhenti. Dan jika Anda memblokir thread utama terlalu lama, aplikasi bahkan dapat mengalami error dan menampilkan dialog Aplikasi Tidak Merespons.
Tonton video di bawah untuk pengantar mengenai cara coroutine mengatasi masalah ini untuk kami di Android dengan memperkenalkan main-safety.
Pola callback
Satu pola untuk melakukan tugas yang berjalan lama tanpa memblokir thread utama adalah callback. Dengan menggunakan callback, Anda dapat memulai tugas yang berjalan lama pada thread latar belakang. Saat tugas selesai, callback akan dipanggil untuk memberi tahu Anda tentang hasilnya pada thread utama.
Lihat contoh pola callback.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Karena dianotasi dengan @UiThread
, kode ini harus berjalan cukup cepat untuk dieksekusi pada thread utama. Artinya, aplikasi harus kembali dengan sangat cepat, sehingga update layar berikutnya tidak tertunda. Namun, karena slowFetch
akan selesai dalam hitungan detik atau bahkan menit, thread utama tidak dapat menunggu hingga hasilnya. Callback show(result)
memungkinkan slowFetch
berjalan pada thread latar belakang dan menampilkan hasilnya saat sudah siap.
Menggunakan coroutine untuk menghapus callback
Callback adalah pola yang bagus, tetapi memiliki beberapa kekurangan. Kode yang sering menggunakan callback dapat menjadi sulit dibaca dan lebih sulit untuk diprediksi. Selain itu, callback tidak mengizinkan penggunaan beberapa fitur bahasa, seperti pengecualian.
Coroutine Kotlin memungkinkan Anda mengonversi kode berbasis callback menjadi kode berurutan. Kode yang ditulis secara berurutan biasanya lebih mudah dibaca, dan bahkan dapat menggunakan fitur bahasa seperti pengecualian.
Pada akhirnya, fungsi tersebut akan melakukan hal yang sama: menunggu hingga hasil tersedia dari tugas yang berjalan lama dan melanjutkan eksekusi. Namun, dalam kode tersebut tampilannya sangat berbeda.
Kata kunci suspend
adalah cara Kotlin untuk menandai fungsi atau jenis fungsi yang tersedia untuk coroutine. Jika coroutine memanggil fungsi yang ditandai sebagai suspend
, bukan memblokir hingga fungsi tersebut kembali seperti panggilan fungsi normal, fungsi tersebut akan menangguhkan eksekusi hingga hasilnya siap, lalu melanjutkan fungsi tersebut dari hasil terakhir. Sembari ditangguhkan karena menunggu hasil, tindakan ini akan berhenti memblokir rangkaian pesan tempatnya berjalan sehingga fungsi atau coroutine lain dapat berjalan.
Misalnya dalam kode di bawah, makeNetworkRequest()
dan slowFetch()
adalah fungsi suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Sama seperti versi callback, makeNetworkRequest
harus segera ditampilkan dari thread utama karena ditandai @UiThread
. Ini berarti bahwa metode tersebut biasanya tidak dapat memanggil metode pemblokiran seperti slowFetch
. Di sinilah kata kunci suspend
menghasilkan keajaibannya.
Dibandingkan dengan kode berbasis callback, kode coroutine mencapai hasil yang sama dengan berhenti memblokir thread saat ini dengan lebih sedikit kode. Berkat gaya berurutannya, menjadi mudah untuk merangkai beberapa tugas yang berjalan lama tanpa membuat beberapa callback. Misalnya, kode yang mengambil hasil dari dua endpoint jaringan dan menyimpannya ke database dapat ditulis sebagai fungsi di coroutine tanpa callback. Seperti ini:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
Anda akan memperkenalkan coroutine ke contoh aplikasi di bagian berikutnya.
Dalam latihan ini, Anda akan menulis coroutine untuk menampilkan pesan setelah penundaan. Untuk memulai, pastikan Anda membuka modul start
di Android Studio.
Memahami CoroutineScope
Di Kotlin, semua coroutine berjalan di dalam CoroutineScope
. Cakupan mengontrol masa pakai coroutine melalui tugasnya. Saat Anda membatalkan tugas cakupan, tindakan tersebut akan membatalkan semua coroutine yang dimulai dalam cakupan tersebut. Di Android, Anda dapat menggunakan cakupan untuk membatalkan semua coroutine yang berjalan saat, misalnya, pengguna keluar dari Activity
atau Fragment
. Cakupan juga memungkinkan Anda menentukan dispatcher default. Dispatcher mengontrol thread mana yang menjalankan coroutine.
Untuk coroutine yang dimulai dengan UI, biasanya tepat untuk memulainya di Dispatchers.Main
yang merupakan thread utama di Android. Coroutine yang dimulai pada Dispatchers.Main
tidak akan memblokir thread utama saat ditangguhkan. Karena coroutine ViewModel
hampir selalu mengupdate UI pada thread utama, memulai coroutine pada thread utama akan menghemat pengalihan thread tambahan. Coroutine yang dimulai pada thread Utama dapat mengalihkan petugas operator kapan saja setelah dimulai. Misalnya, metode ini dapat menggunakan dispatcher lain untuk mengurai hasil JSON besar dari thread utama.
Menggunakan viewModelScope
Library lifecycle-viewmodel-ktx
AndroidX menambahkan CoroutineScope ke ViewModel yang dikonfigurasi untuk memulai coroutine terkait UI. Untuk menggunakan library ini, Anda harus menyertakannya dalam file build.gradle (Module: start)
project Anda. Langkah tersebut sudah dilakukan di project codelab.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
Library tersebut menambahkan viewModelScope
sebagai fungsi ekstensi dari class ViewModel
. Cakupan ini terikat dengan Dispatchers.Main
dan akan otomatis dibatalkan jika ViewModel
dihapus.
Beralih dari thread ke coroutine
Di MainViewModel.kt
, temukan TODO berikutnya bersama dengan kode ini:
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
Kode ini menggunakan BACKGROUND ExecutorService
(ditentukan di util/Executor.kt
) untuk berjalan di thread latar belakang. Karena sleep
memblokir thread saat ini, UI akan membekukan UI jika dipanggil pada thread utama. Satu detik setelah pengguna mengklik tampilan utama, snackbar akan diminta.
Anda dapat melihat hal tersebut dengan menghapus BACKGROUND dari kode dan menjalankannya lagi. Indikator lingkaran berputar pemuatan tidak akan ditampilkan dan semuanya akan "melompat" ke status akhir satu detik kemudian.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Ganti updateTaps
dengan kode berbasis coroutine ini yang juga memiliki fungsi yang sama. Anda harus mengimpor launch
dan delay
.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Kode ini melakukan hal yang sama, menunggu satu detik sebelum menampilkan snackbar. Namun, ada beberapa perbedaan penting:
viewModelScope.
launch
akan memulai coroutine dalamviewModelScope
. Ini berarti saat tugas yang kita teruskan keviewModelScope
dibatalkan, semua coroutine dalam tugas/cakupan ini akan dibatalkan. Jika pengguna keluar dari Aktivitas sebelumdelay
ditampilkan, coroutine ini akan otomatis dibatalkan saatonCleared
dipanggil setelah penghancuran ViewModel.- Karena
viewModelScope
memiliki dispatcher defaultDispatchers.Main
, coroutine ini akan diluncurkan di thread utama. Kita akan melihat cara menggunakan thread yang berbeda nanti. - Fungsi
delay
adalah fungsisuspend
. Ini ditampilkan di Android Studio dengan ikondi gutter kiri. Meskipun coroutine ini berjalan pada thread utama,
delay
tidak akan memblokir thread selama satu detik. Sebaliknya, dispatcher akan menjadwalkan coroutine untuk dilanjutkan dalam satu detik pada pernyataan berikutnya.
Coba jalankan. Saat Anda mengklik tampilan utama, Anda akan melihat snackbar satu detik kemudian.
Di bagian berikutnya, kita akan mempertimbangkan cara menguji fungsi ini.
Dalam latihan ini, Anda akan menulis pengujian untuk kode yang baru saja Anda tulis. Latihan ini menunjukkan cara menguji coroutine yang berjalan di Dispatchers.Main
menggunakan library kotlinx-coroutines-test. Nanti dalam codelab ini, Anda akan menerapkan pengujian yang berinteraksi dengan coroutine secara langsung.
Meninjau kode yang ada
Buka MainViewModelTest.kt
di folder androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Aturan adalah cara untuk menjalankan kode sebelum dan sesudah eksekusi pengujian di JUnit. Dua aturan digunakan untuk memungkinkan kita menguji MainViewModel dalam pengujian di luar perangkat:
InstantTaskExecutorRule
adalah aturan JUnit yang mengonfigurasiLiveData
untuk menjalankan setiap tugas secara sinkronMainCoroutineScopeRule
adalah aturan khusus dalam codebase ini yang mengonfigurasiDispatchers.Main
untuk menggunakanTestCoroutineDispatcher
darikotlinx-coroutines-test
. Hal ini memungkinkan pengujian memajukan jam virtual untuk pengujian, dan memungkinkan kode menggunakanDispatchers.Main
dalam pengujian unit.
Dalam metode setup
, instance MainViewModel
baru dibuat menggunakan pengujian palsu – ini adalah implementasi palsu dari jaringan dan database yang disediakan dalam kode awal untuk membantu menulis pengujian tanpa menggunakan jaringan atau database sebenarnya.
Untuk pengujian ini, objek palsu hanya diperlukan untuk memenuhi dependensi MainViewModel
. Nanti di codelab ini, Anda akan memperbarui file palsu untuk mendukung coroutine.
Menulis pengujian yang mengontrol coroutine
Tambahkan pengujian baru yang memastikan bahwa ketuk diperbarui satu detik setelah tampilan utama diklik:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Dengan memanggil onMainViewClicked
, coroutine yang baru saja kita buat akan diluncurkan. Pengujian ini memeriksa apakah teks ketukan tetap "0 ketukan" tepat setelah onMainViewClicked
dipanggil, lalu 1 detik kemudian diperbarui menjadi "1 ketukan".
Pengujian ini menggunakan waktu-virtual untuk mengontrol eksekusi coroutine yang diluncurkan oleh onMainViewClicked
. MainCoroutineScopeRule
memungkinkan Anda menjeda, melanjutkan, atau mengontrol eksekusi coroutine yang diluncurkan pada Dispatchers.Main
. Di sini kita memanggil advanceTimeBy(1_000)
yang akan menyebabkan dispatcher utama segera mengeksekusi coroutine yang dijadwalkan untuk dilanjutkan 1 detik kemudian.
Pengujian ini sepenuhnya bersifat deterministik, yang berarti pengujian akan selalu dilakukan dengan cara yang sama. Selain itu, karena memiliki kontrol penuh atas eksekusi coroutine yang diluncurkan pada Dispatchers.Main
, fungsi ini tidak perlu menunggu satu detik hingga nilai ditetapkan.
Menjalankan pengujian yang ada
- Klik kanan nama class
MainViewModelTest
di editor Anda untuk membuka menu konteks. - Di menu konteks, pilih
Run 'MainViewModelTest'
- Untuk eksekusi selanjutnya, Anda dapat memilih konfigurasi pengujian ini dalam konfigurasi di samping tombol
di toolbar. Secara default, konfigurasi akan disebut MainViewModelTest.
Anda akan melihat kartu ujian. Perlu waktu kurang dari satu detik untuk menjalankannya.
Dalam latihan berikutnya, Anda akan mempelajari cara mengonversi dari API callback yang ada untuk menggunakan coroutine.
Pada langkah ini, Anda akan mulai mengonversi repositori untuk menggunakan coroutine. Untuk melakukannya, kita akan menambahkan coroutine ke ViewModel
, Repository
, Room
, dan Retrofit
.
Sebaiknya pahami apa yang harus dilakukan oleh setiap bagian arsitektur sebelum kita mengalihkannya agar menggunakan coroutine.
MainDatabase
mengimplementasikan database menggunakan Room yang menyimpan dan memuatTitle
.MainNetwork
mengimplementasikan API jaringan yang mengambil judul baru. Menggunakan Retrofit untuk mengambil judul.Retrofit
dikonfigurasi untuk menampilkan error atau data tiruan secara acak, tetapi berperilaku seolah-olah sedang membuat permintaan jaringan yang sebenarnya.TitleRepository
mengimplementasikan satu API untuk mengambil atau memuat ulang judul dengan menggabungkan data dari jaringan dan database.MainViewModel
merepresentasikan status layar dan menangani peristiwa. Tindakan ini akan memberi tahu repositori untuk memuat ulang judul saat pengguna mengetuk layar.
Karena permintaan jaringan didorong oleh peristiwa UI dan kita ingin memulai coroutine berdasarkan peristiwa tersebut, tempat alami untuk mulai menggunakan coroutine ada di ViewModel
.
Versi callback
Buka MainViewModel.kt
untuk melihat deklarasi refreshTitle
.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Fungsi ini dipanggil setiap kali pengguna mengklik layar – dan ini akan menyebabkan repositori me-refresh judul dan menulis judul baru ke database.
Implementasi ini menggunakan callback untuk melakukan beberapa hal:
- Sebelum memulai kueri, kueri akan menampilkan indikator lingkaran berputar pemuatan dengan
_spinner.value = true
- Jika mendapatkan hasil, aturan ini akan menghapus indikator lingkaran berputar pemuatan dengan
_spinner.value = false
- Jika mengalami error, kode ini akan memberi tahu snackbar untuk menampilkan dan membersihkan spinner
Perhatikan bahwa callback onCompleted
tidak meneruskan title
. Karena kita menulis semua judul ke database Room
, UI akan diupdate ke judul saat ini dengan mengamati LiveData
yang diupdate oleh Room
.
Dalam pembaruan coroutine, kita akan mempertahankan perilaku yang sama persis. Ini adalah pola yang baik untuk menggunakan sumber data yang dapat diamati seperti database Room
agar UI selalu diperbarui secara otomatis.
Versi coroutine
Mari kita tulis ulang refreshTitle
dengan coroutine.
Karena kita akan segera membutuhkannya, mari kita buat fungsi penangguhan kosong di repositori kami (TitleRespository.kt
). Tentukan fungsi baru yang menggunakan operator suspend
untuk memberi tahu Kotlin bahwa fungsi tersebut berfungsi dengan coroutine.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Setelah menyelesaikan codelab ini, Anda akan mengupdate codelab ini untuk menggunakan Retrofit dan Room untuk mengambil judul baru dan menulisnya ke database menggunakan coroutine. Untuk saat ini, itu hanya akan menghabiskan 500 milidetik untuk berpura-pura melakukan pekerjaan dan kemudian melanjutkan.
Di MainViewModel
, ganti versi callback refreshTitle
dengan versi yang meluncurkan coroutine baru:
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Mari kita ikuti fungsi ini:
viewModelScope.launch {
Sama seperti coroutine untuk memperbarui jumlah ketuk, mulai dengan meluncurkan coroutine baru dalam viewModelScope
. Tindakan ini akan menggunakan Dispatchers.Main
yang tidak masalah. Meskipun refreshTitle
akan membuat permintaan jaringan dan kueri database, aplikasi dapat menggunakan coroutine untuk menampilkan antarmuka main-safe. Artinya, Anda dapat memanggilnya dari thread utama dengan aman.
Karena kita menggunakan viewModelScope
, saat pengguna berpindah dari layar ini, pekerjaan yang dimulai oleh coroutine ini akan dibatalkan secara otomatis. Artinya, server tidak akan membuat permintaan jaringan atau kueri database tambahan.
Beberapa baris kode berikutnya sebenarnya memanggil refreshTitle
di repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Sebelum coroutine ini melakukan apa pun, spinner pemuatan akan dimulai – lalu memanggil refreshTitle
sama seperti fungsi biasa. Namun, karena berfungsi sebagai fungsi penangguhan, refreshTitle
dijalankan secara berbeda dari fungsi normal.
Kita tidak harus meneruskan callback. Coroutine akan ditangguhkan hingga dilanjutkan oleh refreshTitle
. Selagi terlihat seperti panggilan fungsi pemblokiran biasa, proses ini akan otomatis menunggu hingga kueri jaringan dan database selesai sebelum melanjutkan tanpa memblokir thread utama.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Pengecualian dalam fungsi penangguhan berfungsi seperti error pada fungsi biasa. Jika Anda menampilkan error dalam fungsi penangguhan, error tersebut akan ditampilkan kepada pemanggil. Jadi, meskipun dieksekusi secara berbeda, Anda dapat menggunakan blok try/catch reguler untuk menanganinya. Ini berguna karena memungkinkan Anda mengandalkan dukungan bahasa bawaan untuk penanganan kesalahan daripada membuat penanganan kesalahan khusus untuk setiap callback.
Dan, jika Anda melemparkan pengecualian dari coroutine – coroutine tersebut akan membatalkan induknya secara default. Artinya, membatalkan beberapa tugas terkait secara mudah dapat dilakukan.
Lalu, dalam blok terakhir, kita bisa memastikan bahwa spinner selalu dinonaktifkan setelah kueri berjalan.
Jalankan aplikasi lagi dengan memilih konfigurasi start lalu menekan , Anda akan melihat indikator lingkaran berputar pemuatan saat Anda mengetuk di mana saja. Judul akan tetap sama karena kami belum menghubungkan jaringan atau database kami.
Pada latihan berikutnya, Anda akan memperbarui repositori untuk benar-benar melakukan pekerjaan.
Dalam latihan ini, Anda akan mempelajari cara mengalihkan thread yang dijalankan coroutine untuk mengimplementasikan versi TitleRepository
yang berfungsi.
Tinjau kode callback yang ada di refreshTitle
Buka TitleRepository.kt
dan tinjau implementasi berbasis callback yang ada.
TitleRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
Dalam TitleRepository.kt
, metode refreshTitleWithCallbacks
diimplementasikan dengan callback untuk mengomunikasikan status pemuatan dan error kepada pemanggil.
Fungsi ini melakukan beberapa hal untuk menerapkan pembaruan.
- Beralih ke rangkaian pesan lain dengan
BACKGROUND
ExecutorService
- Jalankan permintaan jaringan
fetchNextTitle
menggunakan metodeexecute()
pemblokiran. Perintah ini akan menjalankan permintaan jaringan di thread saat ini, dalam hal ini salah satu thread diBACKGROUND
. - Jika hasilnya berhasil, simpan ke database dengan
insertTitle
dan panggil metodeonCompleted()
. - Jika hasilnya tidak berhasil, atau ada pengecualian, panggil metode onError untuk memberi tahu penelepon tentang pemuatan ulang yang gagal.
Implementasi berbasis callback ini bersifat main-safe karena tidak akan memblokir thread utama. Namun, metode ini harus menggunakan callback untuk memberi tahu penelepon saat pekerjaan selesai. Fungsi ini juga memanggil callback pada thread BACKGROUND
yang juga dialihkan.
Memanggil panggilan pemblokiran dari coroutine
Tanpa memperkenalkan coroutine ke jaringan atau database, kita dapat membuat kode ini aman-utama menggunakan coroutine. Ini akan memungkinkan kita membuang callback dan memungkinkan kita meneruskan hasilnya kembali ke thread yang awalnya memanggilnya.
Anda dapat menggunakan pola ini kapan saja perlu melakukan pemblokiran atau pekerjaan intensif CPU dari dalam coroutine seperti mengurutkan dan memfilter daftar besar atau membaca dari disk.
Untuk beralih antara petugas operator, coroutine menggunakan withContext
. Memanggil withContext
akan mengalihkan ke petugas operator lain hanya untuk lambda kemudian kembali ke petugas operator yang memanggilnya dengan hasil lambda tersebut.
Secara default, coroutine Kotlin menyediakan tiga Petugas Operator: Main
, IO
, dan Default
. Petugas operator IO dioptimalkan untuk pekerjaan IO seperti membaca dari jaringan atau disk, sedangkan petugas operator Default dioptimalkan untuk tugas yang menggunakan CPU secara intensif.
TitleRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Implementasi ini menggunakan pemblokiran panggilan untuk jaringan dan database – tetapi masih sedikit lebih sederhana daripada versi callback.
Kode ini masih menggunakan panggilan pemblokiran. Memanggil execute()
dan insertTitle(...)
akan memblokir thread yang menjalankan coroutine ini. Namun, dengan beralih ke Dispatchers.IO
menggunakan withContext
, kami memblokir salah satu rangkaian pesan di petugas operator IO. Coroutine yang memanggil ini, mungkin berjalan pada Dispatchers.Main
, akan ditangguhkan hingga lambda withContext
selesai.
Dibandingkan dengan versi callback, ada dua perbedaan penting:
withContext
akan menampilkan hasilnya ke Dispatcher yang memanggilnya, dalam hal iniDispatchers.Main
. Versi callback yang memanggil callback pada thread di layanan eksekutorBACKGROUND
.- Pemanggil tidak harus meneruskan callback ke fungsi ini. Pengguna dapat mengandalkan penangguhan dan melanjutkan untuk mendapatkan hasil atau error.
Menjalankan kembali aplikasi
Jika menjalankan aplikasi lagi, Anda akan melihat bahwa implementasi berbasis coroutine baru memuat hasil dari jaringan.
Pada langkah berikutnya, Anda akan mengintegrasikan coroutine ke dalam Room dan Retrofit.
Untuk melanjutkan integrasi coroutine, kita akan menggunakan dukungan untuk fungsi penangguhan dalam Room dan Retrofit versi stabil, lalu menyederhanakan kode yang baru saja kita tulis secara substansial dengan menggunakan fungsi penangguhan.
Coroutine di Room
Pertama, buka MainDatabase.kt
dan jadikan insertTitle
sebagai fungsi penangguhan:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Saat melakukannya, Room akan membuat kueri Anda main-safe dan menjalankannya di thread latar belakang secara otomatis. Namun, ini juga berarti bahwa Anda hanya dapat memanggil kueri ini dari dalam coroutine.
Dan – hanya itu yang perlu Anda lakukan untuk menggunakan coroutine di Room. Lumayan bagus.
Coroutine di Retrofit
Berikutnya mari kita lihat cara mengintegrasikan coroutine dengan Retrofit. Buka MainNetwork.kt
dan ubah fetchNextTitle
menjadi fungsi penangguhan.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Untuk menggunakan fungsi penangguhan dengan Retrofit, Anda harus melakukan dua hal:
- Tambahkan pengubah penangguhan ke fungsi
- Hapus wrapper
Call
dari jenis nilai yang ditampilkan. Di sini kita menampilkanString
, tetapi Anda juga dapat menampilkan jenis yang didukung json yang kompleks. Jika masih ingin memberikan akses keResult
lengkap retrofit, Anda dapat menampilkanResult<String>
, bukanString
dari fungsi penangguhan.
Retrofit akan otomatis membuat fungsi penangguhan menjadi main-safe sehingga Anda dapat memanggilnya langsung dari Dispatchers.Main
.
Menggunakan Room dan Retrofit
Setelah Room dan Retrofit mendukung fungsi penangguhan, kita dapat menggunakannya dari repositori. Buka TitleRepository.kt
dan lihat bagaimana penggunaan fungsi penangguhan sangat menyederhanakan logika, bahkan dibandingkan dengan versi pemblokiran:
JudulRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Wow, itu banyak lebih pendek. Apa yang terjadi? Ternyata, mengandalkan penangguhan dan melanjutkan membuat kode jauh lebih singkat. Retrofit memungkinkan kita menggunakan jenis nilai yang ditampilkan seperti objek String
atau User
di sini, bukan Call
. Tindakan ini aman dilakukan, karena di dalam fungsi penangguhan, Retrofit
dapat menjalankan permintaan jaringan di thread latar belakang dan melanjutkan coroutine saat panggilan selesai.
Lebih baik lagi, kami menghapus withContext
. Karena Room dan Retrofit menyediakan fungsi penangguhan yang aman-aman, pengaturan alur asinkron ini dari Dispatchers.Main
menjadi aman.
Memperbaiki error compiler
Pindah ke coroutine memerlukan perubahan tanda tangan fungsi karena Anda tidak dapat memanggil fungsi penangguhan dari fungsi biasa. Saat menambahkan pengubah suspend
pada langkah ini, beberapa error compiler dihasilkan yang menunjukkan apa yang akan terjadi jika Anda mengubah fungsi untuk menangguhkan project sebenarnya.
Periksa project dan perbaiki error compiler dengan mengubah fungsi untuk menangguhkan pembuatan. Berikut adalah resolusi cepat untuk masing-masing:
TestingFakes.kt
Perbarui pengujian palsu untuk mendukung pengubah penangguhan baru.
TitleDaoFake
- Tekan alt-enter untuk menambahkan pengubah penangguhan ke semua fungsi di heiranchy
MainNetworkFake
- Tekan alt-enter untuk menambahkan pengubah penangguhan ke semua fungsi di heiranchy
- Ganti
fetchNextTitle
dengan fungsi ini
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Tekan alt-enter untuk menambahkan pengubah penangguhan ke semua fungsi di heiranchy
- Ganti
fetchNextTitle
dengan fungsi ini
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Hapus fungsi
refreshTitleWithCallbacks
karena tidak digunakan lagi.
Menjalankan aplikasi
Jalankan lagi aplikasi, setelah dikompilasi, Anda akan melihat bahwa aplikasi memuat data menggunakan coroutine dari ViewModel ke Room dan Retrofit!
Selamat, Anda telah sepenuhnya menukar aplikasi ini dengan menggunakan coroutine. Sebagai penutup, kami akan sedikit membahas cara menguji apa yang baru saja kami lakukan.
Dalam latihan ini, Anda akan menulis pengujian yang memanggil fungsi suspend
secara langsung.
Karena refreshTitle
diekspos sebagai API publik, API ini akan diuji secara langsung, yang menunjukkan cara memanggil fungsi coroutine dari pengujian.
Berikut adalah fungsi refreshTitle
yang Anda implementasikan di latihan terakhir:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Menulis pengujian yang memanggil fungsi penangguhan
Buka TitleRepositoryTest.kt
di folder test
yang memiliki dua TODO.
Coba panggil refreshTitle
dari pengujian pertama whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Karena refreshTitle
adalah fungsi suspend
, Kotlin tidak tahu cara memanggilnya kecuali dari coroutine atau fungsi penangguhan lainnya, dan Anda akan mendapatkan error compiler seperti, "Fungsi penangguhan refreshTitle harus dipanggil hanya dari coroutine atau fungsi penangguhan lainnya."
Runner pengujian tidak mengetahui apa pun tentang coroutine sehingga kami tidak dapat menjadikan pengujian ini sebagai fungsi penangguhan. Kita dapat launch
coroutine menggunakan CoroutineScope
seperti pada ViewModel
, tetapi pengujian harus menjalankan coroutine agar selesai sebelum kembali. Setelah fungsi pengujian ditampilkan, pengujian telah berakhir. Coroutine yang dimulai dengan launch
adalah kode asinkron, yang dapat selesai di masa mendatang. Oleh karena itu, untuk menguji kode asinkron tersebut, Anda memerlukan beberapa cara untuk memberi tahu pengujian agar menunggu hingga coroutine selesai. Karena launch
adalah panggilan non-pemblokiran, itu berarti segera kembali dan dapat terus menjalankan coroutine setelah fungsi ditampilkan - fungsi tersebut tidak dapat digunakan dalam pengujian. Misalnya:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Pengujian ini terkadang akan gagal. Panggilan ke launch
akan segera ditampilkan dan dijalankan secara bersamaan dengan kasus uji lainnya. Pengujian tidak memiliki cara untuk mengetahui apakah refreshTitle
telah berjalan atau belum – dan pernyataan apa pun seperti memeriksa apakah database telah diupdate akan menjadi tidak stabil. Dan, jika refreshTitle
melempar pengecualian, pengecualian tidak akan ditampilkan dalam stack panggilan pengujian. Sebaliknya, nilai tersebut akan ditampilkan ke pengendali pengecualian yang tidak tertangkap GlobalScope
.
Library kotlinx-coroutines-test
memiliki fungsi runBlockingTest
yang melakukan pemblokiran saat memanggil fungsi penangguhan. Saat runBlockingTest
memanggil fungsi penangguhan atau launches
coroutine baru, fungsi ini akan langsung dijalankan secara default. Anda dapat menganggapnya sebagai cara untuk mengonversi fungsi penangguhan dan coroutine menjadi panggilan fungsi normal.
Selain itu, runBlockingTest
akan memunculkan kembali pengecualian yang tidak tertangkap untuk Anda. Hal ini mempermudah pengujian saat coroutine menampilkan pengecualian.
Mengimplementasikan pengujian dengan satu coroutine
Gabungkan panggilan ke refreshTitle
dengan runBlockingTest
dan hapus wrapper GlobalScope.launch
dari subject.refreshTitle().
TitleRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
Pengujian ini menggunakan objek palsu yang diberikan untuk memeriksa apakah "OK" disisipkan ke database oleh refreshTitle
.
Saat pengujian memanggil runBlockingTest
, pengujian akan diblokir hingga coroutine yang dimulai oleh runBlockingTest
selesai. Kemudian, di dalam refreshTitle
, kita memanggil mekanisme penangguhan dan melanjutkan reguler untuk menunggu baris database ditambahkan ke palsu.
Setelah coroutine pengujian selesai, runBlockingTest
akan ditampilkan.
Menulis pengujian waktu tunggu
Kita ingin menambahkan waktu tunggu singkat untuk permintaan jaringan. Mari kita tulis pengujiannya terlebih dahulu, lalu terapkan waktu tunggu. Buat pengujian baru:
TitleRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
Pengujian ini menggunakan MainNetworkCompletableFake
palsu yang diberikan, yaitu jaringan palsu yang dirancang untuk menangguhkan penelepon hingga pengujian melanjutkannya. Saat refreshTitle
mencoba membuat permintaan jaringan, permintaan tersebut akan hang selamanya karena kita ingin menguji waktu tunggu.
Kemudian, coroutine diluncurkan untuk memanggil refreshTitle
. Ini adalah bagian penting dari waktu tunggu pengujian, waktu tunggu harus terjadi dalam coroutine yang berbeda dari yang dibuat runBlockingTest
. Dengan demikian, kita dapat memanggil baris berikutnya, advanceTimeBy(5_000)
yang akan memajukan waktu selama 5 detik dan menyebabkan waktu tunggu coroutine lainnya habis.
Ini adalah pengujian waktu tunggu yang lengkap, dan akan lulus setelah kita mengimplementasikan waktu tunggu.
Jalankan sekarang dan lihat apa yang terjadi:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Salah satu fitur runBlockingTest
adalah tidak akan mengizinkan Anda membocorkan coroutine setelah pengujian selesai. Jika ada coroutine yang belum selesai, seperti coroutine peluncuran kami, di akhir pengujian, pengujian tersebut akan gagal.
Menambahkan waktu tunggu
Buka TitleRepository
dan tambahkan waktu tunggu lima detik ke pengambilan jaringan. Anda dapat melakukannya menggunakan fungsi withTimeout
:
TitleRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Jalankan pengujian. Saat menjalankan pengujian, Anda akan melihat semua pengujian lulus.
Dalam latihan berikutnya, Anda akan mempelajari cara menulis fungsi yang lebih tinggi menggunakan coroutine.
Dalam latihan ini, Anda akan memfaktorkan ulang refreshTitle
di MainViewModel
untuk menggunakan fungsi pemuatan data umum. Bagian ini akan mengajari Anda cara membuat fungsi tingkat tinggi yang menggunakan coroutine.
Penerapan refreshTitle
saat ini berfungsi, tetapi kita dapat membuat coroutine pemuatan data umum yang selalu menampilkan spinner. Hal ini mungkin berguna dalam codebase yang memuat data sebagai respons terhadap beberapa peristiwa, dan ingin memastikan indikator lingkaran berputar pemuatan ditampilkan secara konsisten.
Meninjau implementasi saat ini setiap baris kecuali repository.refreshTitle()
adalah boilerplate untuk menampilkan error spinner dan display.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Menggunakan coroutine dalam fungsi urutan yang lebih tinggi
Menambahkan kode ini ke MainViewModel.kt
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Sekarang faktorkan refreshTitle()
untuk menggunakan fungsi urutan yang lebih tinggi ini.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Dengan memisahkan logika dalam menampilkan indikator lingkaran berputar pemuatan dan saat terjadi error, kami telah menyederhanakan kode sebenarnya yang diperlukan untuk memuat data. Menampilkan indikator lingkaran berputar atau menampilkan kesalahan adalah sesuatu yang mudah untuk digeneralisasi ke pemuatan data apa pun, sedangkan sumber data dan tujuan sebenarnya perlu ditentukan setiap saat.
Untuk membuat abstraksi ini, launchDataLoad
menggunakan argumen block
yang merupakan lambda penangguhan. Lambda penangguhan memungkinkan Anda memanggil fungsi penangguhan. Begitulah cara Kotlin mengimplementasikan builder coroutine launch
dan runBlocking
yang telah kami gunakan dalam codelab ini.
// suspend lambda
block: suspend () -> Unit
Untuk membuat lambda penangguhan, mulai dengan kata kunci suspend
. Panah fungsi dan jenis nilai yang ditampilkan Unit
melengkapi deklarasi.
Anda tidak sering harus mendeklarasikan lambda penangguhan Anda sendiri, tetapi langkah ini berguna untuk membuat abstraksi seperti ini yang mengenkapsulasi logika berulang.
Dalam latihan ini, Anda akan mempelajari cara menggunakan kode berbasis coroutine dari WorkManager.
Apa itu WorkManager
Ada banyak opsi di Android untuk pekerjaan latar belakang yang dapat ditangguhkan. Latihan ini menunjukkan cara mengintegrasikan WorkManager dengan coroutine. WorkManager adalah library yang kompatibel, fleksibel, dan sederhana untuk pekerjaan latar belakang yang dapat ditangguhkan. WorkManager adalah solusi yang direkomendasikan untuk kasus penggunaan ini di Android.
WorkManager adalah bagian dari Android Jetpack, dan Komponen Arsitektur untuk pekerjaan latar belakang yang memerlukan kombinasi eksekusi oportunistik dan terjamin. Eksekusi oportunistik berarti WorkManager akan melakukan pekerjaan latar belakang Anda sesegera mungkin. Eksekusi terjamin berarti WorkManager akan menangani logika untuk memulai pekerjaan dalam berbagai situasi, meskipun Anda keluar dari aplikasi.
Karena itu, WorkManager adalah pilihan tepat untuk tugas yang harus diselesaikan pada akhirnya.
Beberapa contoh tugas yang menggunakan WorkManager dengan baik:
- Mengupload log
- Menerapkan filter ke gambar dan menyimpan gambar
- Menyinkronkan data lokal dengan jaringan secara berkala
Menggunakan coroutine dengan WorkManager
WorkManager memberikan implementasi yang berbeda dari class ListanableWorker
dasarnya untuk kasus penggunaan yang berbeda.
Class Pekerja paling sederhana memungkinkan kita menjalankan beberapa operasi sinkron yang dijalankan oleh WorkManager. Namun, setelah sejauh ini mengonversi codebase untuk menggunakan coroutine dan fungsi penangguhan, cara terbaik untuk menggunakan WorkManager adalah melalui class CoroutineWorker
yang memungkinkan penentuan fungsi doWork()
sebagai fungsi penangguhan.
Untuk memulai, buka RefreshMainDataWork
. Class ini sudah memperluas CoroutineWorker
, dan Anda perlu menerapkan doWork
.
Di dalam fungsi suspend
doWork
, panggil refreshTitle()
dari repositori dan tampilkan hasil yang sesuai.
Setelah Anda menyelesaikan TODO, kode akan terlihat seperti ini:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
Perhatikan bahwa CoroutineWorker.doWork()
adalah fungsi penangguhan. Tidak seperti class Worker
yang lebih sederhana, kode ini TIDAK berjalan pada Executor yang ditentukan di konfigurasi WorkManager Anda, tetapi menggunakan dispatcher di anggota coroutineContext
(secara default Dispatchers.Default
).
Menguji CoroutineWorker kita
Tidak ada codebase yang lengkap tanpa pengujian.
WorkManager menyediakan beberapa cara untuk menguji class Worker
Anda. Untuk mempelajari infrastruktur pengujian asli lebih lanjut, Anda dapat membaca dokumentasi.
WorkManager v2.1 memperkenalkan kumpulan API baru untuk mendukung cara yang lebih sederhana untuk menguji class ListenableWorker
dan, akibatnya, CoroutineWorker. Dalam kode, kita akan menggunakan salah satu API baru ini: TestListenableWorkerBuilder
.
Untuk menambahkan pengujian baru, update file RefreshMainDataWorkTest
pada folder androidTest
.
Isi file adalah:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Sebelum memulai pengujian, kita memberi tahu WorkManager
tentang factory agar kita dapat memasukkan jaringan palsu.
Pengujian itu sendiri menggunakan TestListenableWorkerBuilder
untuk membuat pekerja, yang kemudian dapat kita jalankan untuk memanggil metode startWork()
.
WorkManager hanyalah salah satu contoh bagaimana coroutine dapat digunakan untuk menyederhanakan desain API.
Dalam codelab ini, kita telah membahas dasar-dasar yang diperlukan untuk mulai menggunakan coroutine di aplikasi Anda.
Kita membahas:
- Cara mengintegrasikan coroutine ke aplikasi Android dari tugas UI dan WorkManager untuk menyederhanakan pemrograman asinkron,
- Cara menggunakan coroutine di dalam
ViewModel
untuk mengambil data dari jaringan dan menyimpannya ke database tanpa memblokir thread utama. - Serta cara membatalkan semua coroutine saat
ViewModel
selesai.
Untuk menguji kode berbasis coroutine, kita membahas perilaku pengujian serta memanggil fungsi suspend
secara langsung dari pengujian.
Pelajari lebih lanjut
Lihat codelab &Coroutine Tingkat Lanjut dengan Alur Kotlin dan LiveData" untuk mempelajari penggunaan coroutine lanjutan di Android.
Coroutine Kotlin memiliki banyak fitur yang tidak tercakup dalam codelab ini. Jika Anda tertarik mempelajari coroutine Kotlin lebih lanjut, baca panduan coroutine yang dipublikasikan oleh JetBrains. Lihat juga "Tingkatkan performa aplikasi dengan coroutine Kotlin" untuk pola penggunaan coroutine lainnya di Android.