Menggunakan coroutine Kotlin dengan komponen yang mendukung siklus proses

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, gunakan androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0 atau yang lebih baru.
  • Untuk LifecycleScope, gunakan androidx.lifecycle:lifecycle-runtime-ktx:2.2.0 atau yang lebih baru.
  • Untuk liveData, gunakan androidx.lifecycle:lifecycle-livedata-ktx:2.2.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. Coroutine sangat berguna di sini saat Anda memiliki pekerjaan yang harus dilakukan hanya jika ViewModel aktif. Misalnya, jika Anda menghitung beberapa data untuk tata letak, sebaiknya tentukan cakupan pekerjaan tersebut ke ViewModel agar saat ViewModel dihapus, pekerjaan akan otomatis dibatalkan untuk menghindari pemakaian 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 akan dibatalkan saat Lifecycle dihancurkan. 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 dalam status DESTROYED, mungkin ada kasus lain ketika Anda ingin memulai eksekusi blok kode saat Lifecycle berada dalam status tertentu, dan membatalkannya saat berada dalam status lain. Misalnya, Anda mungkin ingin mengumpulkan alur saat Lifecycle dalam status STARTED dan membatalkan pengumpulan saat dalam status 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 berada setidaknya 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
                }
            }
        }
    }
}

Menangguhkan coroutine berbasis 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 dalam status STARTED. Untuk kasus ini, Lifecycle menyediakan metode tambahan: lifecycle.whenCreated, lifecycle.whenStarted, dan lifecycle.whenResumed. Setiap coroutine yang dijalankan dalam blok ini akan 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:

Referensi lainnya

Untuk mempelajari lebih lanjut penggunaan coroutine dengan komponen yang mendukung siklus proses, lihat referensi tambahan berikut.

Contoh

Blog