Bergabunglah bersama kami di ⁠#Android11: The Beta Launch Show pada tanggal 3 Juni!

Menggunakan coroutine Kotlin dengan komponen Arsitektur

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 arsitektur memberikan dukungan kelas satu bagi coroutine untuk cakupan logis di aplikasi Anda bersama lapisan interoperabilitas dengan LiveData. Topik ini menjelaskan cara menggunakan coroutine secara efektif dengan komponen Arsitektur.

Menambahkan dependensi KTX

Cakupan coroutine bawaan yang dijelaskan dalam topik ini dimuat dalam ekstensi KTX untuk setiap komponen Arsitektur yang sesuai. Pastikan untuk menambahkan dependensi yang sesuai saat menggunakan cakupan ini.

  • Untuk ViewModelScope, gunakan androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 atau yang lebih tinggi.
  • Untuk LifecycleScope, gunakan androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 atau yang lebih tinggi.
  • Untuk liveData, gunakan androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 atau yang lebih tinggi.

Cakupan coroutine berbasis Lifecycle

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

Menangguhkan coroutine berbasis Lifecycle

Meskipun CoroutineScope menyediakan cara yang tepat untuk membatalkan operasi yang berjalan lama secara otomatis, mungkin ada kasus lain di mana 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 terkait 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)
    }
    

Blok pembangun 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 CancelationException), 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 arsitektur, lihat referensi tambahan berikut.

Contoh

Blog