Artikel ini bertujuan untuk menunjukkan cara menggunakan Kotlin Coroutines dan menghapus Reaxtive eXtensions (Rx) .
Manfaat
Untuk memulai, mari pertimbangkan empat manfaat Coroutine daripada Rx:
Penangguhan lebih dari pemblokiran
Untuk menjalankan kode non-pemblokiran menggunakan Rx Anda akan menulis sesuatu seperti ini:
Observable.interval(1, TimeUnit.SECONDS) .subscribe { textView.text = "$it seconds have passed" }
Yang secara efektif membuat utas baru. Utas adalah benda berat dalam hal memori dan kinerja.
Keduanya sangat penting dalam dunia pengembangan ponsel.
Anda dapat mencapai perilaku yang sama menggunakan cuplikan berikut:
launch { var i = 0 while (true){ textView.text = "${it++} seconds have passed" delay(1000) } }
Intinya, Coroutine adalah utas yang ringan tapi kami tidak membuat utas nyata.
Di sini kita menggunakan fungsi non-blocking delay (), yang merupakan fungsi penangguhan khusus yang tidak memblokir utas tetapi menangguhkan Coroutine.
Penanganan backpressure alami melalui manual
Backpressure adalah ketika barang yang diamati menghasilkan barang lebih cepat daripada pengamat mereka mengonsumsinya.
Saat menggunakan Rx Anda harus secara eksplisit menentukan bagaimana Anda akan berurusan dengan tekanan balik.
Ada 2 pendekatan dasar:
- Gunakan throttling, buffer atau operator windows
- Model tarik reaktif
Sedangkan Coroutine dapat ditangguhkan mereka memberikan jawaban alami untuk menangani tekanan balik.
Dengan demikian, tidak diperlukan tindakan tambahan.
Sinkronkan gaya kode melalui async
Sifat dasar aplikasi seluler adalah bereaksi terhadap tindakan pengguna. Itulah sebabnya tegangan reaktif akan menjadi pilihan yang baik.
Namun, Anda harus menulis kode dengan gaya fungsional. Jika Anda terbiasa menulis dengan gaya imperatif, itu bisa agak sulit.
Sedangkan Coroutines memungkinkan Anda untuk menulis kode async seolah-olah itu fungsi sinkronisasi biasa. Misalnya,
suspend fun showTextFromRemote() { val text = remote.getText() textView.text = text }
Bahkan saya bekerja dengan gaya fungsional untuk waktu yang lama masih lebih mudah untuk membaca dan men-debug kode penting.
Asli lebih dari 3 lib pihak
Coroutine adalah fitur bawaan asli Kotlin.
Anda tidak perlu menambahkan dependensi tambahan. Saat ini, semua perpustakaan utama dapat menangani coroutine.
Misalnya,
Retrofit
interface Api { @Get("users") suspend fun loadUsers() : List<User> }
Kamar
interface Dao { @Update suspend fun update(user: UserEntity) }
Jadi, Anda dapat membangun aplikasi yang menangguhkan - mulai lapisan UI, melalui domain dan berakhir di lapisan data.
Aplikasi
Mari kita turun ke bisnis. Kami akan membuat aplikasi master detail klasik.
Halaman pertama akan berisi daftar pengiriman yang tak terbatas.
Pada klik item, kami akan membuka halaman detail.
Kami juga akan mendukung mode offline - semua data akan di-cache.
Selain itu, saya akan menggunakan arsitektur MVVM di mana peran ViewModel dimainkan oleh Fragment alih-alih ViewModel dari AAC. Ada beberapa alasan:
Fragmen biasanya sangat botak - hanya mengikat viewModel ke XML.
Fitur seperti pengaturan warna bilah status tidak dapat dilakukan di AAC ViewModel - Anda harus memicu metode fragmen. Menggunakan fragmen sebagai ViewModel akan memungkinkan kita untuk menyimpan semua fungsi terkait (mengelola satu layar yang diberikan) dalam satu kelas.
Pertama, mari kita buat BaseViewModel:
abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO){ protected abstract val layoutId: Int protected abstract val bindings: B protected lateinit var viewBinding: V override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { viewBinding = DataBindingUtil.inflate(inflater, layoutId, container, false) return viewBinding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.lifecycleOwner = viewLifecycleOwner viewBinding.setVariable(BR.bindings, bindings) } override fun onDestroy() { cancel() super.onDestroy() } }
Kami menandai ViewModel kami sebagai CoroutineScope sehingga kami dapat memulai coroutine di dalam model tampilan dan setiap coroutine yang diluncurkan akan terbatas pada siklus hidup sebuah fragmen.
Kita harus secara eksplisit menentukan metode cancel()
siklus hidup siklus panggilan untuk membatalkan semua permintaan yang berjalan untuk menghindari kebocoran memori.
Kami menetapkan retainInstance = true
sehingga dalam konfigurasi perubahan fragmen tidak akan dibuat kembali sehingga kami dapat menyelesaikan semua permintaan yang sudah berjalan lama.
Selain itu, kita harus mengatur lifecycleOwner ke pengikatan untuk mengaktifkan pengikatan data dua arah .
Penanganan pengecualian
Menurut dokumentasi Coroutines:
Coroutine builders come in two flavors: propagating exceptions automatically (launch and actor) or exposing them to users (async and produce). The former treat exceptions as unhandled, similar to Java's Thread.uncaughtExceptionHandler
Karena kami menggunakan launcher builder dalam kebanyakan kasus, kami harus menentukan CoroutineExceptionHandler
CoroutineExceptionHandler adalah CoroutineContext.Element yang dapat digunakan untuk membangun konteks coroutine menggunakan operator plus.
Saya akan mendeklarasikan pengendali statis sebagai berikut:
val exceptionHandler = CoroutineExceptionHandler { _, throwable -> Timber.e(throwable) }
Dan ubah BaseViewModel:
abstract class BaseViewModel<B : BaseBindings, V : ViewDataBinding> : Fragment(), CoroutineScope by CoroutineScope(Dispatchers.IO + exceptionHandler)
Dari sini, pengecualian apa pun yang terjadi di coroutine yang diluncurkan di dalam ruang lingkup ViewModel akan dikirimkan ke handler yang diberikan.
Selanjutnya, saya perlu mendeklarasikan API dan DAO saya:
interface DeliveriesApi { @GET("deliveries") suspend fun getDeliveries(@Query("offset") offset: Int, @Query("limit") limit: Int): List<DeliveryResponse> } @Dao interface DeliveryDao { @Query("SELECT * FROM ${DeliveryEntity.TABLE_NAME}") fun getAll(): DataSource.Factory<Int, DeliveryEntity> @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(delivery: DeliveryEntity) }
Seperti yang Anda lihat, saya menandai metode sebagai ditangguhkan sehingga kami dapat mendeklarasikan objek respons yang diharapkan. Selain itu, pembatalan coroutine induk juga akan membatalkan panggilan jaringan.
Hal yang sama untuk DAO.
Satu-satunya perbedaan adalah bahwa kami ingin memberikan kemampuan untuk mengamati basis data.
Cara termudah adalah dengan menggunakan dukungan data langsung bawaan. Tetapi jika kita akan menandai getAll () sebagai ditangguhkan itu akan menyebabkan kesalahan kompilasi
kesalahan:
Not sure how to convert a Cursor to this method's return type ...
Di sini kita tidak perlu menunda karena:
- Permintaan Db dilakukan di latar belakang secara default
- LiveData yang dihasilkan adalah siklus hidup yang sadar sehingga kita tidak perlu membatalkannya secara manual
Kami harus menggabungkan sumber data jarak jauh dan lokal.
Layak untuk diingat - harus ada satu-satunya titik kebenaran.
Menurut desain offline-pertama , itu akan menjadi penyimpanan lokal. Jadi, kami akan mengamati status basis data. Ketika tidak ada yang dapat diambil, kami akan meminta data dari jarak jauh dan memasukkannya ke database.
Kami akan memperkenalkan kelas Daftar
data class Listing<T>( val pagedList: LiveData<PagedList<T>>, val dataState: LiveData<DataState>, val refreshState: LiveData<DataState>, val refresh: () -> Unit, val retry: () -> Unit )
Mari kita pergi val demi val:
- pagedList - data utama yang dibangun sebagai PagedList untuk mengaktifkan pengguliran tak terbatas dan dibungkus dengan LiveData untuk memungkinkan pengamatan data
- dataState - salah satu dari tiga negara di mana data kita bisa: Sukses, Berjalan, Kesalahan. Juga dibungkus dengan LiveData untuk mengamati perubahan
- refreshState - saat kami memicu penyegaran data melalui swipe-to-refresh, kami memerlukan beberapa alat yang dengannya kami akan membedakan antara umpan balik permintaan menyegarkan dan umpan balik permintaan halaman berikutnya. Untuk yang pertama, kami ingin menunjukkan kesalahan di akhir daftar tetapi untuk kesalahan penyegaran, kami ingin menampilkan pesan roti panggang dan menyembunyikan loader.
- refresh () - callback untuk memicu swipe-to-refresh
coba lagi () - panggil balik untuk memicu kesalahan pemuatan pagedList
Selanjutnya, model tampilan daftar:
class DeliveryListViewModel : BaseViewModel<DeliveryListBindings, DeliveryListBinding>(), DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings { override val layoutId: Int = R.layout.delivery_list override val bindings: DeliveryListBindings = this private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) } private val listing = deliveryGateway.getDeliveries() override val dataState = listing.dataState override val isRefreshing = Transformations.switchMap(listing.refreshState) { MutableLiveData(it == DataState.Loading) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupList() setupRefresh() } private fun setupList() { val adapter = DeliveriesAdapter(this, this) viewBinding.deliveries.adapter = adapter viewBinding.deliveries.setHasFixedSize(true) listing.pagedList.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) listing.dataState.observe(viewLifecycleOwner, Observer { adapter.updateDataState(it) }) } private fun setupRefresh() { listing.refreshState.observe(viewLifecycleOwner, Observer { if (it is DataState.Error) { Toast.makeText(context, it.message, LENGTH_SHORT).show() } }) } override fun refresh() { listing.refresh() } override fun onDeliveryClicked(delivery: Delivery) { view?.findNavController()?.navigate(DeliveryListViewModelDirections.toDetails(delivery)) } override fun onRetryClicked() { listing.retry() } }
Mari kita mulai dari deklarasi kelas.
Pertama-tama, DeliveryListBindings dan DeliveryListBinding. Pertama adalah antarmuka yang dinyatakan untuk merekatkan model tampilan dengan tampilan XML. Kedua adalah kelas autogenerated berdasarkan XML. Kita membutuhkan yang kedua untuk mengatur antarmuka binding dan siklus hidup kita ke XML.
Selain itu, adalah praktik yang baik untuk mereferensikan pandangan menggunakan pengikatan autogenerated ini daripada menggunakan sintetik kotlin.
Mungkin ada kasus ketika dirujuk melalui tampilan sintetis tidak ada dalam tampilan saat ini. Dengan pengikatan data, Anda akan gagal dengan cepat bahkan pada tahap kompilasi.
Berikutnya, tiga antarmuka: DeliveryListBindings, DeliveryListItemBindings, DeliveryListErrorBindings.
- DeliveryListBindings - binding untuk layar itu sendiri. Misalnya, ini berisi metode refresh () yang dipanggil dengan gesekan vertikal.
- DeliveryListItemBindings - binding untuk item dalam daftar. Misalnya, onClicked ()
- DeliveryListErrorBindings - binding untuk tampilan kesalahan yang juga merupakan item daftar yang ditunjukkan pada status kesalahan. Misalnya, ini berisi metode coba lagi ()
Dengan demikian, kami menangani semuanya dalam model tampilan tunggal karena ini adalah layar tunggal tetapi juga mengikuti prinsip Segregasi Antarmuka
Mari kita mengalihkan perhatian khusus ke baris ini:
private val deliveryGateway: DeliveryGateway by inject { parametersOf(this) }
DeliveryGateway perlu melakukan permintaan dari utas utama. Jadi, perlu mendeklarasikan metode sebagai ditangguhkan atau CoroutineScope untuk meluncurkan coroutine baru pada ruang lingkup ini. Kami akan memilih pendekatan kedua karena kami membutuhkan LiveData kami dari awal dan kemudian kami hanya akan menunggu pembaruan darinya. Ini sangat mirip dengan berlangganan ke instance liveData ketika kita melewati lifecycleOwner (yang sering merujuk pada 'ini'). Berikut adalah cara yang sama kita melewati 'ini' sebagai CoroutineScope
Antarmuka CoroutineScope terdiri dari bidang tunggal - CoroutineContext. Intinya, ruang lingkup dan konteks adalah hal yang sama. Perbedaan antara konteks dan ruang lingkup adalah dalam tujuan yang dimaksudkan.
Untuk mempelajari lebih lanjut tentang ini, saya akan merekomendasikan sebuah artikel oleh Roman Elizarov. Jadi, menyediakan ruang lingkup untuk DeliveryGateway juga akan menghasilkan penggunaan konteks yang sama. Khususnya penangan utas, pekerjaan, dan pengecualian.
Sekarang mari kita lihat DeliveryGateway itu sendiri:
class DeliveryBoundGateway( private val db: DataBase, private val api: DeliveriesApi, private val deliveryDao: DeliveryDao, private val coroutineScope: CoroutineScope ) : DeliveryGateway { private val boundaryCallback = DeliveriesBoundaryCallback( api = api, coroutineScope = coroutineScope, handleResponse = { insertIntoDatabase(it) } ) @MainThread override fun getDeliveries(): Listing<Delivery> { val refreshTrigger = MutableLiveData<Unit>() val refreshState = Transformations.switchMap(refreshTrigger) { refresh() } val pagingConfig = Config( initialLoadSizeHint = PAGE_SIZE, pageSize = PAGE_SIZE, prefetchDistance = PAGE_SIZE ) val deliveries = deliveryDao.getAll() .toLiveData( config = pagingConfig, boundaryCallback = boundaryCallback ) return Listing( pagedList = deliveries, dataState = boundaryCallback.dataState, retry = { boundaryCallback.helper.retryAllFailed() }, refresh = { refreshTrigger.value = null }, refreshState = refreshState ) } @MainThread private fun refresh(): LiveData<DataState> { boundaryCallback.refresh() val dataState = MutableLiveData<DataState>() dataState.value = DataState.Loading coroutineScope.launch { try { val deliveries = api.getDeliveries(0, PAGE_SIZE) db.withTransaction { deliveryDao.clear() insertIntoDatabase(deliveries) } dataState.postValue(DataState.Loaded) } catch (throwable: Throwable) { Timber.w(throwable) dataState.postValue(DataState.Error(throwable.message)) } } return dataState } private suspend fun insertIntoDatabase(deliveries: List<DeliveryResponse>) { deliveries.forEach { delivery -> val entity = deliveryConverter.fromNetwork(delivery) deliveryDao.insert(entity) } } companion object { const val PAGE_SIZE = 20 } }
Di sini kita sedang membangun struktur LiveData dari awal dan kemudian menggunakan coroutines memuat data dan mempostingnya ke LiveData. Kami juga menggunakan implementasi PagedList.BoundaryCallback () untuk merekatkan basis data lokal dan API jarak jauh. Ketika kita mencapai akhir batas daftar pagedCallback dipicu dan memuat potongan data berikutnya.
Seperti yang Anda lihat, kami menggunakan coroutineScope untuk meluncurkan coroutine baru.
Karena ruang lingkup ini sama dengan siklus hidup fragmen - semua permintaan yang tertunda akan dibatalkan pada panggilan balik onDestroy()
fragmen.
Halaman detail pengiriman cukup mudah - kami hanya melewatkan objek Pengiriman sebagai Parcelable dari layar utama menggunakan komponen navigasi, simpan plugin args. Pada layar detail cukup ikat diberi objek ke XML.
class DeliveryViewModel : BaseViewModel<DeliveryBindings, DeliveryBinding>(), DeliveryBindings { override val layoutId: Int = R.layout.delivery override val bindings: DeliveryBindings = this private val args: DeliveryViewModelArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewBinding.delivery = args.delivery viewBinding.image.clipToOutline = true } }
Berikut ini tautan ke kode sumber github.
Anda dipersilakan untuk meninggalkan komentar dan masalah terbuka.