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,
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 } } } } 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>() { β¦ fun emitAndExecute(event: NavigationEvent) = newEvent(event, Type.EXECUTE_WITHOUT_LIMITS) fun emitAndExecuteOnce(event: NavigationEvent) = newEvent(event, Type.EXECUTE_ONCE) fun waitAndExecute(event: NavigationEvent) = newEvent(event, Type.WAIT_OBSERVER_IF_NEEDED) 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() 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() ..
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)
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 .