Mengirim acara dari ViewModel ke Aktivitas / Fragmen di MVVM

Hari ini kita akan berbicara tentang bagaimana bertukar acara antara Aktivitas / Fragmen dan ViewModel di MVVM. Untuk mendapatkan data dari ViewModel, disarankan agar dalam Activity / Fragment berlangganan data LiveData yang ditemukan di ViewModel. Tetapi apa yang harus dilakukan untuk mengirim acara tunggal (dan tidak hanya), seperti menunjukkan pemberitahuan atau, misalnya, membuka fragmen lain?



Halo semuanya!

Nama saya Alexei, saya adalah pengembang android di Home Credit Bank.

Dalam artikel ini, saya akan membagikan cara kami mengirim dan menerima acara dari ViewModels ke Views (Kegiatan / Fragmen).

Dalam aplikasi kami "Barang Angsuran dari Bank Kredit Rumah" kami menggunakan fragmen, oleh karena itu kami akan membicarakannya, tetapi semuanya juga relevan untuk Aktivitas.

Apa yang kita inginkan?


Kami memiliki Fragmen, yang mencakup beberapa ViewModel, data terikat oleh DataBinding . Semua acara pengguna menerima ViewModel. Jika acara tersebut adalah acara navigasi: Anda perlu membuka fragmen / aktivitas lain, tunjukkan AlertDialog , Snackbar , permintaan sistem untuk Izin, dll., Maka acara seperti itu harus dijalankan dalam fragmen.

Dan apa sebenarnya masalahnya?


Siklus hidup Tampilan dan ViewModels tidak terkait. Tidak mungkin mengikat dengan pendengar, karena ViewModel seharusnya tidak tahu apa-apa tentang fragmen, dan juga tidak boleh mengandung tautan ke fragmen, jika tidak, seperti yang Anda tahu, memori akan mulai " bersukacita ".

Pendekatan standar untuk interaksi antara Fragmen dan ViewModels adalah berlangganan LiveData , yang terletak di ViewModel. LiveData mungkin mengirimkan acara secara langsung melalui LiveData , karena fakta bahwa pendekatan ini tidak memperhitungkan apakah acara tersebut telah selesai atau belum.

Solusi apa yang ada:


1. Gunakan SingleLiveEvent
Pro: acara dilaksanakan sekali.
Cons: satu acara - satu SingleLiveEvent . Dengan sejumlah besar acara, objek-objek N muncul di ViewModel, yang masing-masingnya harus berlangganan dalam fragmen.

2. Contoh yang bagus.
Kelebihan: satu acara juga dijalankan sekali, Anda dapat mentransfer data dari viewModel ke fragmen.
Cons: data dalam acara tersebut diperlukan, tetapi jika Anda perlu menjalankan suatu acara tanpa data (val content: T) , Anda perlu membuat kelas lain. Itu tidak memecahkan masalah mengeksekusi satu jenis acara sekali (acara itu sendiri dijalankan sekali, tetapi jenis acara ini akan dieksekusi sebanyak yang kita jalankan dari ViewModel). Misalnya, permintaan-N berjalan secara serempak, tetapi tidak ada jaringan. Setiap permintaan akan kembali dengan kesalahan jaringan, dan akan menarik fragmen peristiwa N tentang kesalahan jaringan, peringatan N akan terbuka di fragmen. Pengguna tidak akan menyetujui keputusan seperti itu :). Kami harus menunjukkan kepadanya sekali pesan dengan kesalahan ini. Dengan kata lain, jenis peristiwa ini harus dilaksanakan sekali.

Solusi


Kami mengambil ide SingleLiveEvent untuk menyimpan informasi penanganan acara sebagai dasar.

Tentukan kemungkinan jenis acara


 enum class Type { EXECUTE_WITHOUT_LIMITS, //    –   ,             EXECUTE_ONCE, //      WAIT_OBSERVER_IF_NEEDED,//    ,     -       WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE //    ,     -           } 

Buat kelas dasar acara - NavigationEvent


isHandled menunjukkan apakah acara diterima (kami percaya bahwa itu dilaksanakan jika Pengamat diterima dalam fragmen).

 open class NavigationEvent(var isHandled: Boolean = false, var type: Events.Type) 

Buat kelas Emitter - Emitter


Kelas emitor acara mewarisi dari LiveData < NavigationEvent >. Kelas ini akan digunakan dalam ViewModel untuk mengirim acara.

 class Emitter : MutableLiveData<NavigationEvent>() { private val waitingEvents: ArrayList<NavigationEvent> = ArrayList() private var isActive = false override fun onInactive() { isActive = false } override fun onActive() { isActive = true val postingEvents = ArrayList<NavigationEvent>() waitingEvents .forEach { if (hasObservers()) { this.value = it postingEvents.add(it) } }.also { waitingEvents.removeAll(postingEvents) } } private fun newEvent(event: NavigationEvent, type: Type) { event.type = type this.value = when (type) { Type.EXECUTE_WITHOUT_LIMITS, Type.EXECUTE_ONCE -> if (hasObservers()) event else null Type.WAIT_OBSERVER_IF_NEEDED, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (hasObservers() && isActive) event else { waitingEvents.add(event) null } } } } /** Clear All Waiting Events */ fun clearWaitingEvents() = waitingEvents.clear() } 

isActive - kita perlu memahami apakah setidaknya satu Observer berlangganan Emitter . Dan dalam kasus ketika pelanggan telah muncul dan acara yang menunggunya telah terkumpul, kami mengirim acara ini. Klarifikasi penting: mengirim acara tidak perlu melalui this.postValue(event) , tetapi melalui setter this.value = event . Jika tidak, pelanggan hanya akan menerima acara terakhir dalam daftar.

Metode untuk mengirim acara baru, newEvent(event, type) , menerima dua parameter - pada kenyataannya, acara itu sendiri dan jenis acara ini.

Agar tidak mengingat semua jenis acara (nama panjang), kami akan membuat metode publik terpisah yang hanya akan menerima acara itu sendiri:

 class Emitter : MutableLiveData<NavigationEvent>() { … /** Default: Emit Event for Execution */ fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) /** Emit Event for Execution Once */ fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) /** Wait Observer Available and Emit Event for Execution */ fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) /** Wait Observer Available and Emit Event for Execution Once */ fun waitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE) } 

Secara formal, Anda sudah dapat berlangganan Emitter di ViewModel dan menerima acara tanpa memperhatikan penanganannya (baik acara tersebut sudah diproses atau belum).

Buat Kelas Pengamat Acara - EventObserver


 class EventObserver(private val handlerBlock: (NavigationEvent) -> Unit) : Observer<NavigationEvent> { private val executedEvents: HashSet<String> = hashSetOf() /** Clear All Executed Events */ fun clearExecutedEvents() = executedEvents.clear() override fun onChanged(it: NavigationEvent?) { when (it?.type) { Type.EXECUTE_WITHOUT_LIMITS, Type.WAIT_OBSERVER_IF_NEEDED -> { if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } } Type.EXECUTE_ONCE, Type.WAIT_OBSERVER_IF_NEEDED_AND_EXECUTE_ONCE -> { if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } } } } } 

Pengamat ini menerima fungsi tingkat tinggi sebagai input - pemrosesan peristiwa akan ditulis dalam fragmen (contoh di bawah).

Metode clearExecutedEvents() untuk membersihkan acara yang dieksekusi (yang seharusnya dieksekusi sekali). Diperlukan saat memperbarui layar, misalnya, di swipeToRefresh() .

Sebenarnya, metode onChange() utama, yang terjadi ketika data emitor baru diterima, tempat pengamat ini berlangganan.

Dalam hal acara memiliki jenis eksekusi dalam jumlah tak terbatas, maka kami memeriksa apakah acara tersebut telah dieksekusi dan memprosesnya. Kami menjalankan acara dan menunjukkan bahwa itu diterima dan diproses.

 if (!it.isHandled) { it.isHandled = true it.apply(handlerBlock) } 

Jika acara adalah tipe yang harus dieksekusi sekali, maka periksa apakah kelas acara ini ada di tabel hash. Jika tidak, jalankan acara dan tambahkan kelas acara ini ke tabel hash.

 if (it.javaClass.simpleName !in executedEvents) { if (!it.isHandled) { it.isHandled = true executedEvents.add(it.javaClass.simpleName) it.apply(handlerBlock) } } 

Tetapi bagaimana cara mentransfer data di dalam acara?


Untuk melakukan ini, antarmuka MyFragmentNavigation , yang akan terdiri dari kelas yang diwarisi dari NavigationEvent() . Berbagai kelas dibuat di bawah ini dengan dan tanpa parameter lulus.

 interface MyFragmentNavigation { class ShowCategoryList : NavigationEvent() class OpenProduct(val productId: String, val productName: String) : NavigationEvent() class PlayVideo(val url: String) : NavigationEvent() class ShowNetworkError : NavigationEvent() } 

Cara kerjanya dalam praktik


Mengirim acara dari ViewModel:

 class MyViewModel : ViewModel() { val emitter = Events.Enitter() fun doOnShowCategoryListButtonClicked() = emitter.emitAndExecute(MyNavigation.ShowCategoryList()) fun doOnPlayClicked() = emitter.waitAndExecuteOnce(MyNavigation.PlayVideo(url = "https://site.com/abc")) fun doOnProductClicked() = emitter.emitAndExecute(MyNavigation.OpenProduct( productId = "123", productTitle = " Samsung") ) fun doOnNetworkError() = emitter.emitAndExecuteOnce(MyNavigation.ShowNetworkError()) fun doOnSwipeRefresh(){ emitter.clearWaitingEvents() ..//loadData() } } 

Menerima acara secara terpisah:

 class MyFragment : Fragment() { private val navigationEventsObserver = Events.EventObserver { event -> when (event) { is MyFragmentNavigation.ShowCategoryList -> ShowCategoryList() is MyFragmentNavigation.PlayVideo -> videoPlayerView.loadUrl(event.url) is MyFragmentNavigation.OpenProduct -> openProduct(id = event.productId, name = event.otherInfo) is MyFragmentNavigation.ShowNetworkError -> showNetworkErrorAlert() } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Observer   ViewModels     myViewModel.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelSecond.emitter.observe(viewLifecycleOwner, navigationEventsObserver) myViewModelThird.emitter.observe(viewLifecycleOwner, navigationEventsObserver) } private fun ShowCategoryList(){ ... } private fun openProduct(id: String, name: String){ ... } private fun showNetworkErrorAlert(){ ... } } 

Intinya, kami mendapatkan analog Rx- BehaviorSubjec ta dan EventBus -a, hanya berdasarkan LiveData , di mana Emitter dapat mengumpulkan acara sebelum penampilan artis-Pengamat, dan di mana Pengamat dapat memantau jenis acara dan, jika perlu, memanggilnya hanya sekali.

Selamat datang di komentar dengan saran.

Tautan ke sumber .
Paket cicilan dari Home Credit Bank .

Source: https://habr.com/ru/post/id477314/


All Articles