Coroutine Kotlin menyediakan API yang memungkinkan Anda menulis kode asinkron. Dengan coroutine Kotlin, Anda dapat menentukan CoroutineScope
, yang membantu Anda mengelola kapan coroutine harus dijalankan. Setiap operasi asinkron berjalan dalam cakupan tertentu.
Komponen yang mendukung siklus proses memberikan dukungan terbaik bagi coroutine untuk cakupan logis di aplikasi Anda bersama lapisan interoperabilitas dengan LiveData
. Topik ini menjelaskan cara menggunakan coroutine secara efektif dengan komponen yang mendukung siklus proses.
Menambahkan dependensi KTX
Cakupan coroutine bawaan yang dijelaskan dalam topik ini dimuat dalam ekstensi KTX untuk setiap komponen yang sesuai. Pastikan untuk menambahkan dependensi yang sesuai saat menggunakan cakupan ini.
- Untuk
ViewModelScope
, gunakanandroidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
atau yang lebih baru. - Untuk
LifecycleScope
, gunakanandroidx.lifecycle:lifecycle-runtime-ktx:2.4.0
atau yang lebih baru. - Untuk
liveData
, gunakanandroidx.lifecycle:lifecycle-livedata-ktx:2.4.0
atau yang lebih baru.
Cakupan coroutine yang mendukung siklus proses
Komponen yang mendukung siklus proses menentukan cakupan bawaan berikut yang dapat Anda gunakan di aplikasi.
ViewModelScope
ViewModelScope
ditentukan untuk setiap ViewModel
di aplikasi Anda. Setiap coroutine yang diluncurkan dalam cakupan ini akan otomatis dibatalkan jika ViewModel
dihapus. Coroutines sangat berguna di sini untuk jika Anda memiliki pekerjaan yang harus dilakukan hanya saat ViewModel
aktif. Misalnya, jika Anda menghitung beberapa data untuk tata letak, sebaiknya tentukan cakupan pekerjaan tersebut ke ViewModel
agar saat ViewModel
dihapus, pekerjaan otomatis dibatalkan untuk menghindari konsumsi resource.
Anda dapat mengakses CoroutineScope
dari ViewModel
melalui properti viewModelScope
ViewModel, seperti yang ditunjukkan pada contoh berikut:
class MyViewModel: ViewModel() { init { viewModelScope.launch { // Coroutine that will be canceled when the ViewModel is cleared. } } }
LifecycleScope
LifecycleScope
ditentukan untuk setiap objek Lifecycle
. Setiap coroutine yang diluncurkan dalam cakupan ini dibatalkan saat Lifecycle
dihapus. Anda dapat mengakses CoroutineScope
dari Lifecycle
baik melalui properti lifecycle.coroutineScope
maupun lifecycleOwner.lifecycleScope
.
Contoh di bawah menunjukkan cara menggunakan lifecycleOwner.lifecycleScope
untuk membuat teks prakomputasi secara asinkron:
class MyFragment: Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { val params = TextViewCompat.getTextMetricsParams(textView) val precomputedText = withContext(Dispatchers.Default) { PrecomputedTextCompat.create(longTextContent, params) } TextViewCompat.setPrecomputedText(textView, precomputedText) } } }
Coroutine berbasis Siklus proses yang dapat dimulai ulang
Meskipun lifecycleScope
menyediakan cara yang tepat untuk otomatis membatalkan operasi yang berjalan lama saat Lifecycle
adalah DESTROYED
, mungkin ada kasus lain saat Anda ingin memulai eksekusi blok kode ketika Lifecycle
berada dalam status tertentu, dan membatalkannya ketika Lifecycle
berada dalam status lain. Misalnya, Anda mungkin ingin mengumpulkan alur saat Lifecycle
adalah STARTED
dan membatalkan koleksi saat Lifecycle
adalah STOPPED
. Pendekatan ini memproses emisi alur hanya saat UI terlihat di layar, menghemat resource, dan berpotensi menghindari error aplikasi.
Untuk kasus ini, Lifecycle
dan LifecycleOwner
menyediakan API repeatOnLifecycle
yang ditangguhkan yang benar-benar melakukan hal tersebut. Contoh berikut berisi blok kode yang berjalan setiap kali Lifecycle
terkait setidaknya berada dalam status STARTED
dan dibatalkan saat Lifecycle
dalam status STOPPED
:
class MyFragment : Fragment() { val viewModel: MyViewModel by viewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Create a new coroutine in the lifecycleScope viewLifecycleOwner.lifecycleScope.launch { // repeatOnLifecycle launches the block in a new coroutine every time the // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Trigger the flow and start listening for values. // This happens when lifecycle is STARTED and stops // collecting when the lifecycle is STOPPED viewModel.someDataFlow.collect { // Process item } } } } }
Pengumpulan alur berbasis Siklus proses
Jika hanya perlu melakukan pengumpulan yang mendukung siklus proses di satu alur, Anda dapat menggunakan metode Flow.flowWithLifecycle()
untuk menyederhanakan kode:
viewLifecycleOwner.lifecycleScope.launch { exampleProvider.exampleFlow() .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .collect { // Process the value. } }
Namun, jika perlu melakukan pengumpulan yang mendukung siklus proses di beberapa alur secara paralel, Anda harus mengumpulkan setiap alur di coroutine yang berbeda. Karenanya, akan lebih efisien untuk menggunakan repeatOnLifecycle()
secara langsung:
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Because collect is a suspend function, if you want to // collect multiple flows in parallel, you need to do so in // different coroutines. launch { flow1.collect { /* Process the value. */ } } launch { flow2.collect { /* Process the value. */ } } } }
Menangguhkan coroutine yang mendukung Siklus proses
Meskipun CoroutineScope
menyediakan cara yang tepat untuk otomatis membatalkan operasi yang berjalan lama, mungkin ada kasus lain saat Anda ingin menangguhkan eksekusi blok kode kecuali Lifecycle
berada dalam status tertentu. Misalnya, untuk menjalankan FragmentTransaction
, Anda harus menunggu hingga Lifecycle
setidaknya STARTED
. Untuk kasus ini, Lifecycle
menyediakan metode tambahan: lifecycle.whenCreated
, lifecycle.whenStarted
, dan lifecycle.whenResumed
. Setiap coroutine yang dijalankan dalam blok ini ditangguhkan jika Lifecycle
tidak berada setidaknya dalam status minimal yang diinginkan.
Contoh di bawah ini berisi blok kode yang hanya berjalan jika Lifecycle
teratribusi setidaknya berada dalam status STARTED
:
class MyFragment: Fragment { init { // Notice that we can safely launch in the constructor of the Fragment. lifecycleScope.launch { whenStarted { // The block inside will run only when Lifecycle is at least STARTED. // It will start executing when fragment is started and // can call other suspend methods. loadingView.visibility = View.VISIBLE val canAccess = withContext(Dispatchers.IO) { checkUserAccess() } // When checkUserAccess returns, the next line is automatically // suspended if the Lifecycle is not *at least* STARTED. // We could safely run fragment transactions because we know the // code won't run unless the lifecycle is at least STARTED. loadingView.visibility = View.GONE if (canAccess == false) { findNavController().popBackStack() } else { showContent() } } // This line runs only after the whenStarted block above has completed. } } }
Jika Lifecycle
dihapus saat coroutine aktif melalui salah satu metode when
, coroutine akan otomatis dibatalkan. Pada contoh di bawah ini, blok finally
berjalan setelah status Lifecycle
menjadi DESTROYED
:
class MyFragment: Fragment { init { lifecycleScope.launchWhenStarted { try { // Call some suspend functions. } finally { // This line might execute after Lifecycle is DESTROYED. if (lifecycle.state >= STARTED) { // Here, since we've checked, it is safe to run any // Fragment transactions. } } } } }
Menggunakan coroutine dengan LiveData
Saat menggunakan LiveData
, Anda mungkin perlu menghitung nilai secara asinkron. Misalnya, Anda mungkin ingin mengambil preferensi pengguna dan menayangkannya ke UI Anda. Dalam kasus ini, Anda dapat menggunakan fungsi builder liveData
untuk memanggil fungsi suspend
, yang menayangkan hasilnya sebagai objek LiveData
.
Pada contoh di bawah, loadUser()
adalah fungsi penangguhan yang dideklarasikan di tempat lain. Gunakan fungsi builder liveData
untuk memanggil loadUser()
secara asinkron, lalu gunakan emit()
untuk menampilkan hasilnya:
val user: LiveData<User> = liveData { val data = database.loadUser() // loadUser is a suspend function. emit(data) }
Elemen penyusun dasar liveData
berfungsi sebagai primitif serentak terstruktur antara coroutine dan LiveData
. Blok kode mulai mengeksekusi saat LiveData
menjadi aktif dan otomatis dibatalkan setelah waktu tunggu yang dapat dikonfigurasi saat LiveData
menjadi tidak aktif. Jika dibatalkan sebelum diselesaikan, blok kode akan dimulai ulang jika LiveData
menjadi aktif lagi. Jika berhasil diselesaikan dalam proses sebelumnya, blok kode tidak dimulai ulang. Perlu diperhatikan bahwa blok kode dimulai ulang hanya jika dibatalkan secara otomatis. Jika dibatalkan karena alasan lain (misalnya, menampilkan CancellationException
), blok tidak dimulai ulang.
Anda juga dapat menampilkan beberapa nilai dari blok. Setiap panggilan emit()
menangguhkan eksekusi blok hingga nilai LiveData
disetel pada thread utama.
val user: LiveData<Result> = liveData { emit(Result.loading()) try { emit(Result.success(fetchUser())) } catch(ioException: Exception) { emit(Result.error(ioException)) } }
Anda juga dapat menggabungkan liveData
dengan Transformations
, seperti yang ditunjukkan pada contoh berikut:
class MyViewModel: ViewModel() { private val userId: LiveData<String> = MutableLiveData() val user = userId.switchMap { id -> liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { emit(database.loadUserById(id)) } } }
Anda dapat menampilkan beberapa nilai dari LiveData
dengan memanggil fungsi emitSource()
setiap kali Anda ingin menampilkan nilai baru. Perlu diperhatikan bahwa setiap panggilan ke emit()
atau emitSource()
akan menghapus sumber yang telah ditambahkan sebelumnya.
class UserDao: Dao { @Query("SELECT * FROM User WHERE id = :id") fun getUser(id: String): LiveData<User> } class MyRepository { fun getUser(id: String) = liveData<User> { val disposable = emitSource( userDao.getUser(id).map { Result.loading(it) } ) try { val user = webservice.fetchUser(id) // Stop the previous emission to avoid dispatching the updated user // as `loading`. disposable.dispose() // Update the database. userDao.insert(user) // Re-establish the emission with success type. emitSource( userDao.getUser(id).map { Result.success(it) } ) } catch(exception: IOException) { // Any call to `emit` disposes the previous one automatically so we don't // need to dispose it here as we didn't get an updated value. emitSource( userDao.getUser(id).map { Result.error(exception, it) } ) } } }
Untuk informasi selengkapnya terkait coroutine, lihat link berikut:
- Menyempurnakan performa aplikasi dengan coroutine Kotlin
- Ringkasan coroutine
- Threading di CoroutineWorker
Referensi lainnya
Untuk mempelajari lebih lanjut penggunaan coroutine dengan komponen yang mendukung siklus proses, lihat referensi tambahan berikut.
Contoh
Blog
- Coroutine di Android: Pola aplikasi
- Coroutine mudah di Android: viewModelScope
- Menguji dua emisi LiveData yang berurutan di coroutine
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Ringkasan LiveData
- Menangani Siklus Proses dengan Komponen Berbasis Siklus Proses
- Memuat dan menampilkan data yang dibagi-bagi