Menggunakan Coroutine Kotlin di Aplikasi Android Anda

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

  • Memanggil kode yang ditulis dengan coroutine dan memperoleh hasil.
  • 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.

Mendownload kode

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 yang sering diajukan

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

  1. MainActivity menampilkan UI, mendaftarkan pemroses klik, dan dapat menampilkan Snackbar. Ini akan meneruskan peristiwa ke MainViewModel dan mengupdate 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 berbagai hal 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-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:

  1. viewModelScope.launch akan memulai coroutine dalam viewModelScope. Ini berarti 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 setelah 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. Ini ditampilkan di Android Studio dengan ikon di 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:

  1. InstantTaskExecutorRule adalah aturan JUnit yang mengonfigurasi LiveData untuk menjalankan setiap tugas secara sinkron
  2. MainCoroutineScopeRule adalah aturan khusus dalam codebase ini yang mengonfigurasi Dispatchers.Main untuk menggunakan TestCoroutineDispatcher dari kotlinx-coroutines-test. Hal ini memungkinkan pengujian memajukan jam virtual untuk pengujian, dan memungkinkan kode menggunakan Dispatchers.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

  1. Klik kanan nama class MainViewModelTest di editor Anda untuk membuka menu konteks.
  2. Di menu konteks, pilih execute.pngRun 'MainViewModelTest'
  3. Untuk eksekusi selanjutnya, Anda dapat memilih konfigurasi pengujian ini dalam konfigurasi di samping tombol execute.png 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.

  1. MainDatabase mengimplementasikan database menggunakan Room yang menyimpan dan memuat Title.
  2. 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.
  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. 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 execute.png, 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.

  1. Beralih ke rangkaian pesan lain dengan BACKGROUND ExecutorService
  2. Jalankan permintaan jaringan fetchNextTitle menggunakan metode execute() pemblokiran. Perintah 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 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:

  1. withContext akan menampilkan hasilnya ke Dispatcher yang memanggilnya, dalam hal ini Dispatchers.Main. Versi callback yang memanggil callback pada thread di layanan eksekutor BACKGROUND.
  2. 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:

  1. Tambahkan pengubah penangguhan ke fungsi
  2. Hapus wrapper Call dari jenis nilai yang ditampilkan. Di sini kita menampilkan String, tetapi Anda juga dapat menampilkan jenis yang didukung json yang kompleks. Jika masih ingin memberikan akses ke Result lengkap retrofit, Anda dapat menampilkan Result<String>, bukan String 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

  1. Tekan alt-enter untuk menambahkan pengubah penangguhan ke semua fungsi di heiranchy

MainNetworkFake

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

MainNetworkCompletableFake

  1. Tekan alt-enter untuk menambahkan pengubah penangguhan ke semua fungsi di heiranchy
  2. 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.