Menggunakan Coroutine Kotlin di Aplikasi Android Anda

Dalam codelab ini, Anda akan mempelajari cara menggunakan Coroutine Kotlin di aplikasi Android—cara baru untuk 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 database atau jaringan, menjadi kode berurutan.

Berikut adalah 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 menjadi 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, yang dibuat menggunakan Komponen Arsitektur, yang menggunakan gaya callback untuk tugas yang berjalan lama.

Di akhir codelab ini, Anda akan memiliki cukup pengalaman untuk menggunakan coroutine di aplikasi Anda guna 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, dan Room.
  • 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

  • Panggil kode yang ditulis dengan coroutine dan dapatkan hasilnya.
  • Gunakan fungsi penangguhan untuk membuat kode asinkron berurutan.
  • Gunakan launch dan runBlocking 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.

Download kodenya

Klik link berikut guna mendownload semua kode untuk codelab ini:

Download Zip

... atau clone repositori GitHub dari command line dengan menggunakan perintah berikut:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Pertanyaan umum (FAQ)

Pertama-tama, mari kita lihat bagaimana tampilan aplikasi contoh. Ikuti petunjuk ini untuk membuka aplikasi contoh di Android Studio.

  1. Jika Anda mendownload file zip kotlin-coroutines, ekstrak zip file tersebut.
  2. Buka project coroutines-codelab di Android Studio.
  3. Pilih modul aplikasi start.
  4. Klik tombol execute.pngRun, lalu pilih emulator atau hubungkan perangkat Android Anda, yang harus dapat menjalankan Android Lollipop (SDK minimum yang didukung adalah 21). Layar Kotlin Coroutines akan muncul:

Aplikasi pemula ini menggunakan thread untuk menambah jumlah dengan penundaan singkat setelah Anda menekan layar. Aplikasi ini juga akan mengambil judul baru dari jaringan dan menampilkannya di layar. Coba sekarang, dan Anda akan melihat perubahan jumlah dan pesan setelah penundaan singkat. 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.

  1. MainActivity menampilkan UI, mendaftarkan pemroses klik, dan dapat menampilkan Snackbar. Class ini meneruskan peristiwa ke MainViewModel dan memperbarui layar berdasarkan LiveData di MainViewModel.
  2. MainViewModel menangani peristiwa di onMainViewClicked dan akan berkomunikasi dengan MainActivity menggunakan LiveData.
  3. Executors menentukan BACKGROUND, yang dapat menjalankan sesuatu di thread latar belakang.
  4. 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-coroutines-core — Antarmuka utama untuk menggunakan coroutine di Kotlin
  • kotlinx-coroutines-android — Dukungan untuk thread Utama Android dalam coroutine

Aplikasi starter sudah menyertakan dependensi di 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, sangat penting untuk menghindari pemblokiran thread utama. Thread utama adalah satu thread yang menangani semua update pada UI. Thread ini juga merupakan thread yang memanggil semua pengendali klik dan callback UI lainnya. Oleh karena itu, UI thread harus berjalan lancar untuk menjamin pengalaman pengguna yang baik.

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 dari ini, seperti mengurai set data JSON 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 mengetahui pengantar tentang cara coroutine menyelesaikan masalah ini untuk kita di Android dengan memperkenalkan main-safety.

Pola callback

Salah 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 hasil 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 kode ini dianotasi dengan @UiThread, kode tersebut harus berjalan cukup cepat untuk dieksekusi di thread utama. Artinya, fungsi ini harus ditampilkan dengan sangat cepat, sehingga update layar berikutnya tidak tertunda. Namun, karena slowFetch akan memerlukan waktu beberapa detik atau bahkan menit untuk selesai, thread utama tidak dapat menunggu hasilnya. Callback show(result) memungkinkan slowFetch berjalan di thread latar belakang dan menampilkan hasilnya saat 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, keduanya melakukan hal yang sama persis: menunggu hingga hasil tersedia dari tugas yang berjalan lama dan melanjutkan eksekusi. Namun, dalam kode, keduanya terlihat sangat berbeda.

Kata kunci suspend adalah cara Kotlin menandai fungsi, atau jenis fungsi, yang tersedia untuk coroutine. Jika coroutine memanggil fungsi yang ditandai suspend, daripada memblokir hingga fungsi itu kembali seperti pemanggilan fungsi normal, fungsi tersebut akan menangguhkan eksekusi hingga hasilnya siap, lalu melanjutkan dengan hasil tersebut dari tempat terakhir. Sembari ditangguhkan karena menunggu hasil, fungsi akan membuka blokir thread yang sedang 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 kembali dari thread utama karena ditandai @UiThread. Artinya, biasanya tidak dapat memanggil metode pemblokiran seperti slowFetch. Di sinilah kata kunci suspend bekerja dengan ajaib.

Dibandingkan dengan kode berbasis callback, kode coroutine mencapai hasil yang sama dalam membuka blokir thread saat ini dengan lebih sedikit kode. Karena gaya berurutannya, Anda dapat dengan mudah menggabungkan 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 dalam coroutine tanpa callback. Contoh:

// 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 aplikasi contoh di bagian berikutnya.

Dalam latihan ini, Anda akan menulis coroutine untuk menampilkan pesan setelah penundaan. Untuk memulai, pastikan Anda telah 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 sedang berjalan, misalnya, saat pengguna keluar dari Activity atau Fragment. Cakupan juga memungkinkan Anda menentukan dispatcher default. Dispatcher mengontrol thread mana yang menjalankan coroutine.

Untuk coroutine yang dimulai oleh UI, biasanya tepat untuk memulainya di Dispatchers.Main yang merupakan thread utama di Android. Coroutine yang dimulai di Dispatchers.Main tidak akan memblokir thread utama saat ditangguhkan. Karena coroutine ViewModel hampir selalu mengupdate UI di thread utama, memulai coroutine di thread utama akan menghemat peralihan thread tambahan. Coroutine yang dimulai di thread Utama dapat mengalihkan dispatcher kapan saja setelah dimulai. Misalnya, ia 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 ini menambahkan viewModelScope sebagai fungsi ekstensi class ViewModel. Cakupan ini terikat ke Dispatchers.Main dan akan otomatis dibatalkan saat ViewModel dihapus.

Beralih dari thread ke coroutine

Di MainViewModel.kt, temukan TODO berikutnya beserta 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 dijalankan di thread latar belakang. Karena sleep memblokir thread saat ini, sleep akan membekukan UI jika dipanggil di thread utama. Satu detik setelah pengguna mengklik tampilan utama, tampilan tersebut akan meminta snackbar.

Anda dapat melihatnya dengan menghapus BACKGROUND dari kode dan menjalankannya lagi. Indikator 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 melakukan hal 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:

  1. viewModelScope.launch akan memulai coroutine di viewModelScope. Artinya, saat tugas yang kita teruskan ke viewModelScope dibatalkan, semua coroutine dalam tugas/cakupan ini akan dibatalkan. Jika pengguna keluar dari Aktivitas sebelum delay ditampilkan, coroutine ini akan otomatis dibatalkan saat onCleared dipanggil saat penghancuran ViewModel.
  2. Karena viewModelScope memiliki dispatcher default Dispatchers.Main, coroutine ini akan diluncurkan di thread utama. Kita akan melihat cara menggunakan thread yang berbeda nanti.
  3. Fungsi delay adalah fungsi suspend. Hal ini ditunjukkan di Android Studio dengan ikon di gutter kiri. Meskipun coroutine ini berjalan di thread utama, delay tidak akan memblokir thread selama satu detik. Sebagai gantinya, dispatcher akan menjadwalkan coroutine untuk dilanjutkan dalam satu detik pada pernyataan berikutnya.

Lanjutkan dan 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 langsung dengan coroutine.

Tinjau 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 setelah eksekusi pengujian di JUnit. Dua aturan digunakan untuk memungkinkan kita menguji MainViewModel dalam pengujian di luar perangkat:

  1. InstantTaskExecutorRule adalah aturan JUnit yang mengonfigurasi LiveData untuk menjalankan setiap tugas secara serentak
  2. MainCoroutineScopeRule adalah aturan kustom dalam codebase ini yang mengonfigurasi Dispatchers.Main untuk menggunakan TestCoroutineDispatcher dari kotlinx-coroutines-test. Hal ini memungkinkan pengujian memajukan virtual-clock untuk pengujian, dan memungkinkan kode menggunakan Dispatchers.Main dalam pengujian unit.

Dalam metode setup, instance MainViewModel baru dibuat menggunakan tiruan pengujian – 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 tiruan hanya diperlukan untuk memenuhi dependensi MainViewModel. Nanti di codelab ini, Anda akan mengupdate tiruan untuk mendukung coroutine.

Menulis pengujian yang mengontrol coroutine

Tambahkan pengujian baru yang memastikan bahwa ketukan 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 virtual-time untuk mengontrol eksekusi coroutine yang diluncurkan oleh onMainViewClicked. MainCoroutineScopeRule memungkinkan Anda menjeda, melanjutkan, atau mengontrol eksekusi coroutine yang diluncurkan di Dispatchers.Main. Di sini kita memanggil advanceTimeBy(1_000) yang akan menyebabkan dispatcher utama segera menjalankan coroutine yang dijadwalkan untuk dilanjutkan 1 detik kemudian.

Pengujian ini sepenuhnya deterministik, yang berarti pengujian akan selalu dieksekusi dengan cara yang sama. Selain itu, karena memiliki kontrol penuh atas eksekusi coroutine yang diluncurkan di Dispatchers.Main, ia tidak perlu menunggu satu detik agar nilai ditetapkan.

Menjalankan pengujian yang ada

  1. Klik kanan nama class MainViewModelTest di editor Anda untuk membuka menu konteks.
  2. Di menu konteks, pilih execute.pngRun 'MainViewModelTest'
  3. Untuk menjalankan pengujian di masa mendatang, Anda dapat memilih konfigurasi pengujian ini di konfigurasi di samping tombol execute.png di toolbar. Secara default, konfigurasi akan disebut MainViewModelTest.

Anda akan melihat pengujian berhasil. Waktu yang diperlukan untuk menjalankannya jauh kurang dari satu detik.

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 tugas setiap bagian arsitektur sebelum kita beralih ke penggunaan coroutine.

  1. MainDatabase mengimplementasikan database menggunakan Room yang menyimpan dan memuat Title.
  2. MainNetwork mengimplementasikan API jaringan yang mengambil judul baru. Aplikasi ini menggunakan Retrofit untuk mengambil judul. Retrofit dikonfigurasi untuk menampilkan error atau data tiruan secara acak, tetapi berperilaku seolah-olah membuat permintaan jaringan yang sebenarnya.
  3. TitleRepository mengimplementasikan satu API untuk mengambil atau memuat ulang judul dengan menggabungkan data dari jaringan dan database.
  4. MainViewModel merepresentasikan status layar dan menangani peristiwa. Kode 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 yang tepat untuk mulai menggunakan coroutine adalah 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 akan menyebabkan repositori memuat ulang judul dan menulis judul baru ke database.

Implementasi ini menggunakan callback untuk melakukan beberapa hal:

  • Sebelum memulai kueri, aplikasi menampilkan indikator pemuatan dengan _spinner.value = true
  • Saat mendapatkan hasil, spinner pemuatan akan dihapus dengan _spinner.value = false
  • Jika terjadi error, snackbar akan ditampilkan dan spinner akan dihapus

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 update ke coroutine, kita akan mempertahankan perilaku yang sama persis. Pola yang baik adalah menggunakan sumber data yang dapat diamati seperti database Room untuk otomatis menjaga UI tetap terbaru.

Versi coroutine

Mari kita tulis ulang refreshTitle dengan coroutine.

Karena kita akan segera membutuhkannya, mari buat fungsi penangguhan kosong di repositori kita (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 memperbaruinya untuk menggunakan Retrofit dan Room guna mengambil judul baru dan menuliskannya ke database menggunakan coroutine. Untuk saat ini, fungsi ini hanya akan menghabiskan 500 milidetik untuk berpura-pura melakukan pekerjaan, lalu 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 pelajari fungsi ini:

viewModelScope.launch {

Sama seperti coroutine untuk memperbarui jumlah ketukan, mulailah dengan meluncurkan coroutine baru di viewModelScope. Tindakan ini akan menggunakan Dispatchers.Main yang tidak masalah. Meskipun akan membuat permintaan jaringan dan kueri database, refreshTitle dapat menggunakan coroutine untuk mengekspos antarmuka main-safe. Artinya, aman untuk memanggilnya dari thread utama.

Karena kita menggunakan viewModelScope, saat pengguna keluar dari layar ini, tugas yang dimulai oleh coroutine ini akan otomatis dibatalkan. Artinya, tidak akan membuat permintaan jaringan atau kueri database tambahan.

Beberapa baris kode berikutnya benar-benar memanggil refreshTitle di repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Sebelum melakukan apa pun, coroutine ini akan memulai spinner pemuatan – lalu memanggil refreshTitle seperti fungsi biasa. Namun, karena refreshTitle adalah fungsi penangguhan, eksekusinya berbeda dengan fungsi normal.

Kita tidak perlu meneruskan callback. Coroutine akan ditangguhkan hingga dilanjutkan oleh refreshTitle. Meskipun terlihat seperti panggilan fungsi pemblokiran biasa, fungsi ini akan otomatis menunggu hingga kueri jaringan dan database selesai sebelum dilanjutkan tanpa memblokir thread utama.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Pengecualian dalam fungsi suspend berfungsi seperti error dalam fungsi reguler. Jika Anda menampilkan error dalam fungsi yang ditangguhkan, error tersebut akan ditampilkan ke pemanggil. Jadi, meskipun dieksekusi secara berbeda, Anda dapat menggunakan blok try/catch reguler untuk menanganinya. Hal ini berguna karena memungkinkan Anda mengandalkan dukungan bahasa bawaan untuk penanganan error, alih-alih membuat penanganan error kustom untuk setiap callback.

Selain itu, jika Anda menampilkan pengecualian dari coroutine, coroutine tersebut akan membatalkan induknya secara default. Artinya, Anda dapat membatalkan beberapa tugas terkait secara bersamaan dengan mudah.

Kemudian, di blok finally, kita dapat memastikan bahwa spinner selalu dinonaktifkan setelah kueri berjalan.

Jalankan aplikasi lagi dengan memilih konfigurasi start, lalu tekanexecute.png. Anda akan melihat spinner pemuatan saat mengetuk di mana saja. Judul akan tetap sama karena kita belum menghubungkan jaringan atau database.

Pada latihan berikutnya, Anda akan memperbarui repositori untuk benar-benar melakukan pekerjaan.

Dalam latihan ini, Anda akan mempelajari cara mengganti thread tempat coroutine berjalan untuk menerapkan 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))
       }
   }
}

Di TitleRepository.kt, metode refreshTitleWithCallbacks diimplementasikan dengan callback untuk mengomunikasikan status pemuatan dan error kepada pemanggil.

Fungsi ini melakukan beberapa hal untuk menerapkan pemuatan ulang.

  1. Beralih ke rangkaian pesan lain dengan BACKGROUND ExecutorService
  2. Jalankan permintaan jaringan fetchNextTitle menggunakan metode execute() pemblokiran. Hal ini akan menjalankan permintaan jaringan di thread saat ini, dalam hal ini salah satu thread di BACKGROUND.
  3. Jika hasilnya berhasil, simpan ke database dengan insertTitle dan panggil metode onCompleted().
  4. Jika hasilnya tidak berhasil, atau ada pengecualian, panggil metode onError untuk memberi tahu pemanggil tentang refresh yang gagal.

Implementasi berbasis callback ini main-safe karena tidak akan memblokir thread utama. Namun, fungsi ini harus menggunakan callback untuk memberi tahu pemanggil saat pekerjaan selesai. Kode ini juga memanggil callback pada thread BACKGROUND yang diubahnya.

Memblokir panggilan dari coroutine

Tanpa memperkenalkan coroutine ke jaringan atau database, kita dapat membuat kode ini aman untuk thread utama menggunakan coroutine. Hal ini akan memungkinkan kita menghapus callback dan meneruskan hasilnya kembali ke thread yang awalnya memanggilnya.

Anda dapat menggunakan pola ini kapan saja saat perlu melakukan pekerjaan pemblokiran atau yang membebani CPU dari dalam coroutine seperti menyortir dan memfilter daftar besar atau membaca dari disk.

Untuk beralih antara dispatcher, 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 Dispatcher: 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 panggilan pemblokiran 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 tempat coroutine ini berjalan. Namun, dengan beralih ke Dispatchers.IO menggunakan withContext, kita memblokir salah satu thread di dispatcher IO. Coroutine yang memanggilnya, yang mungkin berjalan di Dispatchers.Main, akan ditangguhkan hingga lambda withContext selesai.

Dibandingkan dengan versi callback, ada dua perbedaan penting:

  1. withContext menampilkan hasilnya kembali ke Dispatcher yang memanggilnya, dalam hal ini Dispatchers.Main. Versi callback memanggil callback pada thread di layanan eksekutor BACKGROUND.
  2. Pemanggil tidak harus meneruskan callback ke fungsi ini. Mereka 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 di Room dan Retrofit versi stabil, lalu menyederhanakan kode yang baru saja kita tulis secara signifikan 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 Anda melakukannya, Room akan membuat kueri Anda aman untuk thread utama dan mengeksekusinya di thread latar belakang secara otomatis. Namun, ini juga berarti Anda hanya dapat memanggil kueri ini dari dalam coroutine.

Dan – hanya itu yang harus Anda lakukan untuk menggunakan coroutine di Room. Cukup bagus.

Coroutine di Retrofit

Selanjutnya, 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:

  1. Menambahkan pengubah penangguhan ke fungsi
  2. Hapus wrapper Call dari jenis nilai yang ditampilkan. Di sini kita menampilkan String, tetapi Anda juga dapat menampilkan jenis kompleks yang didukung json. Jika masih ingin memberikan akses ke Result penuh retrofit, Anda dapat menampilkan Result<String>, bukan String dari fungsi suspend.

Retrofit akan otomatis membuat fungsi penangguhan menjadi main-safe sehingga Anda dapat memanggilnya langsung dari Dispatchers.Main.

Menggunakan Room dan Retrofit

Sekarang karena Room dan Retrofit mendukung fungsi penangguhan, kita dapat menggunakannya dari repositori. Buka TitleRepository.kt dan lihat cara penggunaan fungsi penangguhan yang sangat menyederhanakan logika, bahkan dibandingkan dengan versi pemblokiran:

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)
   }
}

Wow, itu jauh lebih singkat. Apa yang terjadi? Ternyata, mengandalkan penangguhan dan melanjutkan memungkinkan kode menjadi jauh lebih pendek. Retrofit memungkinkan kita menggunakan jenis nilai yang ditampilkan seperti String atau objek 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, kita menyingkirkan withContext. Karena Room dan Retrofit menyediakan fungsi penangguhan main-safe, aman untuk mengatur pekerjaan asinkron ini dari Dispatchers.Main.

Memperbaiki error compiler

Beralih ke coroutine melibatkan perubahan tanda tangan fungsi karena Anda tidak dapat memanggil fungsi penangguhan dari fungsi biasa. Saat Anda menambahkan pengubah suspend pada langkah ini, beberapa error compiler dihasilkan yang menunjukkan apa yang akan terjadi jika Anda mengubah fungsi menjadi ditangguhkan dalam project sebenarnya.

Buka project dan perbaiki error compiler dengan mengubah fungsi menjadi ditangguhkan. Berikut resolusi cepat untuk setiap masalah:

TestingFakes.kt

Perbarui tiruan pengujian untuk mendukung pengubah penangguhan baru.

TitleDaoFake

  1. Tekan alt+enter untuk menambahkan pengubah penangguhan ke semua fungsi dalam hierarki

MainNetworkFake

  1. Tekan alt+enter untuk menambahkan pengubah penangguhan ke semua fungsi dalam hierarki
  2. Ganti fetchNextTitle dengan fungsi ini
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Tekan alt+enter untuk menambahkan pengubah penangguhan ke semua fungsi dalam hierarki
  2. Ganti fetchNextTitle dengan fungsi ini
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Hapus fungsi refreshTitleWithCallbacks karena tidak digunakan lagi.

Menjalankan aplikasi

Jalankan aplikasi lagi, setelah dikompilasi, Anda akan melihat bahwa aplikasi memuat data menggunakan coroutine dari ViewModel hingga Room dan Retrofit.

Selamat, Anda telah sepenuhnya mengganti aplikasi ini untuk menggunakan coroutine. Sebagai penutup, kita akan membahas sedikit cara menguji apa yang baru saja kita lakukan.

Dalam latihan ini, Anda akan menulis pengujian yang memanggil fungsi suspend secara langsung.

Karena diekspos sebagai API publik, refreshTitle akan diuji secara langsung, yang menunjukkan cara memanggil fungsi coroutine dari pengujian.

Berikut fungsi refreshTitle yang Anda terapkan dalam 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 lain, dan Anda akan mendapatkan error compiler seperti, "Suspend function refreshTitle should be called only from a coroutine or another suspend function."

Runner pengujian tidak mengetahui apa pun tentang coroutine sehingga kita tidak dapat menjadikan pengujian ini sebagai fungsi penangguhan. Kita dapat launch coroutine menggunakan CoroutineScope seperti di ViewModel, tetapi pengujian perlu menjalankan coroutine hingga selesai sebelum kembali. Setelah fungsi pengujian ditampilkan, pengujian akan 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 cara untuk memberi tahu pengujian agar menunggu hingga coroutine Anda selesai. Karena launch adalah panggilan non-blocking, artinya launch langsung ditampilkan dan dapat terus menjalankan coroutine setelah fungsi ditampilkan - launch tidak dapat digunakan dalam pengujian. Contoh:

@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 bersamaan dengan bagian pengujian lainnya. Pengujian tidak dapat mengetahui apakah refreshTitle sudah berjalan atau belum – dan pernyataan apa pun seperti memeriksa apakah database telah diperbarui akan tidak stabil. Selain itu, jika refreshTitle menampilkan pengecualian, pengecualian tersebut tidak akan ditampilkan dalam stack panggilan pengujian. Sebagai gantinya, pengecualian akan dilempar ke handler pengecualian yang tidak tertangkap GlobalScope.

Library kotlinx-coroutines-test memiliki fungsi runBlockingTest yang memblokir 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 melempar ulang pengecualian yang tidak tertangkap untuk Anda. Hal ini mempermudah pengujian saat coroutine memunculkan pengecualian.

Menerapkan 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 tiruan yang disediakan untuk memeriksa apakah "OK" dimasukkan ke database oleh refreshTitle.

Saat pengujian memanggil runBlockingTest, pengujian akan diblokir hingga coroutine yang dimulai oleh runBlockingTest selesai. Kemudian di dalamnya, saat kita memanggil refreshTitle, mekanisme penangguhan dan melanjutkan reguler akan digunakan untuk menunggu baris database ditambahkan ke tiruan kita.

Setelah coroutine pengujian selesai, runBlockingTest akan ditampilkan.

Menulis pengujian waktu tunggu

Kita ingin menambahkan waktu tunggu singkat ke permintaan jaringan. Mari kita tulis pengujian 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 disediakan, yang merupakan sumber data palsu jaringan yang dirancang untuk menangguhkan pemanggil hingga pengujian melanjutkannya. Saat refreshTitle mencoba membuat permintaan jaringan, refreshTitle akan terus ditangguhkan karena kita ingin menguji waktu tunggu.

Kemudian, ia meluncurkan coroutine terpisah untuk memanggil refreshTitle. Ini adalah bagian penting dari pengujian waktu tunggu, waktu tunggu harus terjadi di coroutine yang berbeda dengan yang dibuat runBlockingTest. Dengan melakukannya, kita dapat memanggil baris berikutnya, advanceTimeBy(5_000), yang akan memajukan waktu 5 detik dan menyebabkan coroutine lainnya kehabisan waktu.

Ini adalah pengujian waktu tunggu selesai, dan akan lulus setelah kita menerapkan 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 membiarkan Anda membocorkan coroutine setelah pengujian selesai. Jika ada coroutine yang belum selesai, seperti coroutine peluncuran, di akhir pengujian, pengujian akan gagal.

Menambahkan waktu tunggu

Buka TitleRepository dan tambahkan waktu tunggu lima detik ke pengambilan jaringan. Anda dapat melakukannya dengan 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 berhasil.

Pada latihan berikutnya, Anda akan mempelajari cara menulis fungsi tingkat tinggi menggunakan coroutine.

Dalam latihan ini, Anda akan memfaktorkan ulang refreshTitle di MainViewModel untuk menggunakan fungsi pemuatan data umum. Bagian ini akan mengajarkan cara membuat fungsi tingkat tinggi yang menggunakan coroutine.

Implementasi refreshTitle saat ini berfungsi, tetapi kita dapat membuat coroutine pemuatan data umum yang selalu menampilkan indikator lingkaran berputar. Hal ini mungkin berguna dalam codebase yang memuat data sebagai respons terhadap beberapa peristiwa, dan ingin memastikan indikator pemuatan ditampilkan secara konsisten.

Meninjau penerapan saat ini, setiap baris kecuali repository.refreshTitle() adalah boilerplate untuk menampilkan spinner dan menampilkan error.

// 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

Tambahkan 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 refaktorkan refreshTitle() untuk menggunakan fungsi urutan yang lebih tinggi ini.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

Dengan mengabstraksi logika seputar menampilkan indikator lingkaran berputar pemuatan dan menampilkan error, kita telah menyederhanakan kode sebenarnya yang diperlukan untuk memuat data. Menampilkan spinner atau menampilkan error adalah sesuatu yang mudah digeneralisasi ke pemuatan data apa pun, sementara sumber dan tujuan data yang sebenarnya harus ditentukan setiap saat.

Untuk membuat abstraksi ini, launchDataLoad mengambil argumen block yang merupakan lambda yang ditangguhkan. Lambda penangguhan memungkinkan Anda memanggil fungsi penangguhan. Begitulah cara Kotlin mengimplementasikan builder coroutine launch dan runBlocking yang telah kita gunakan dalam codelab ini.

// suspend lambda

block: suspend () -> Unit

Untuk membuat lambda yang ditangguhkan, mulai dengan kata kunci suspend. Panah fungsi dan jenis nilai yang ditampilkan Unit melengkapi deklarasi.

Anda tidak perlu sering mendeklarasikan lambda penangguhan Anda sendiri, tetapi lambda ini dapat membantu membuat abstraksi seperti ini yang merangkum 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 sederhana yang kompatibel dan fleksibel 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.

Oleh 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 menyediakan berbagai penerapan class dasar ListanableWorker untuk berbagai kasus penggunaan.

Class Worker paling sederhana memungkinkan kita menjalankan beberapa operasi sinkron oleh WorkManager. Namun, setelah berupaya mengonversi codebase kita untuk menggunakan coroutine dan fungsi penangguhan, cara terbaik untuk menggunakan WorkManager adalah melalui class CoroutineWorker yang memungkinkan kita menentukan fungsi doWork() sebagai fungsi penangguhan.

Untuk memulai, buka RefreshMainDataWork. Class ini sudah memperluas CoroutineWorker, dan Anda perlu mengimplementasikan doWork.

Di dalam fungsi suspend doWork, panggil refreshTitle() dari repositori dan tampilkan hasil yang sesuai.

Setelah Anda menyelesaikan TODO, kodenya 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. Berbeda dengan class Worker yang lebih sederhana, kode ini TIDAK berjalan di Executor yang ditentukan dalam konfigurasi WorkManager Anda, tetapi menggunakan dispatcher di anggota coroutineContext (secara default Dispatchers.Default).

Menguji CoroutineWorker

Tidak ada codebase yang lengkap tanpa pengujian.

WorkManager menyediakan beberapa cara berbeda untuk menguji class Worker, untuk mempelajari lebih lanjut infrastruktur pengujian asli, Anda dapat membaca dokumentasi.

WorkManager v2.1 memperkenalkan serangkaian API baru untuk mendukung cara yang lebih sederhana dalam menguji class ListenableWorker dan, sebagai konsekuensinya, CoroutineWorker. Dalam kode, kita akan menggunakan salah satu API baru ini: TestListenableWorkerBuilder.

Untuk menambahkan pengujian baru, perbarui file RefreshMainDataWorkTest di folder androidTest.

Isi file tersebut 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 melakukan pengujian, kita memberi tahu WorkManager tentang pabrik agar kita dapat menyuntikkan jaringan palsu.

Pengujian itu sendiri menggunakan TestListenableWorkerBuilder untuk membuat pekerja yang kemudian dapat kita jalankan dengan memanggil metode startWork().

WorkManager hanyalah salah satu contoh cara penggunaan coroutine untuk menyederhanakan desain API.

Dalam codelab ini, kita telah membahas dasar-dasar yang diperlukan untuk mulai menggunakan coroutine di aplikasi Anda.

Kami 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.
  • Dan cara membatalkan semua coroutine saat ViewModel selesai.

Untuk menguji kode berbasis coroutine, kita membahas keduanya dengan menguji perilaku serta memanggil fungsi suspend secara langsung dari pengujian.

Pelajari lebih lanjut

Lihat codelab "Coroutine Lanjutan dengan Flow Kotlin dan LiveData" untuk mempelajari penggunaan coroutine lanjutan di Android.

Coroutine Kotlin memiliki banyak fitur yang tidak dibahas dalam codelab ini. Jika Anda tertarik untuk mempelajari lebih lanjut coroutine Kotlin, baca panduan coroutine yang dipublikasikan oleh JetBrains. Lihat juga "Meningkatkan performa aplikasi dengan coroutine Kotlin" untuk mengetahui pola penggunaan coroutine lainnya di Android.