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