Senden von Ereignissen von ViewModel an Activity / Fragment in MVVM

Heute werden wir darüber sprechen, wie Ereignisse zwischen Aktivitäten / Fragmenten und ViewModel in MVVM ausgetauscht werden. Um Daten aus dem ViewModel abzurufen, wird empfohlen, dass Sie unter Aktivität / Fragment die im ViewModel gefundenen LiveData-Daten abonnieren. Aber was tun, um einzelne (und nicht nur) Ereignisse zu senden, z. B. eine Benachrichtigung anzuzeigen oder beispielsweise ein anderes Fragment zu öffnen?



Hallo allerseits!

Mein Name ist Alexei, ich bin ein Android-Entwickler bei der Home Credit Bank.

In diesem Artikel werde ich unsere Art des Sendens und Empfangens von Ereignissen von ViewModels zu Views (Aktivitäten / Fragmente) beschreiben.

In unserer Anwendung „Ratenzahlung von Waren von der Heimatkreditbank“ verwenden wir Fragmente, daher werden wir darauf eingehen, aber alles ist auch für die Aktivität relevant.

Was wollen wir


Wir haben ein Fragment, das mehrere ViewModel enthält, Daten werden durch DataBinding gebunden. Alle Benutzerereignisse erhalten ViewModel. Wenn es sich bei dem Ereignis um ein Navigationsereignis handelt: Sie müssen ein anderes Fragment / eine andere Aktivität AlertDialog , AlertDialog , Snackbar , eine Systemanforderung für Berechtigungen usw. AlertDialog . Dann sollte ein solches Ereignis in dem Fragment ausgeführt werden.

Und wo liegt eigentlich das Problem?


Ansichten und ViewModels-Lebenszyklen haben keine Beziehung zueinander. Es ist unmöglich, sich an Listener zu binden, da ViewModel nichts über Fragmente wissen und auch keinen Link zu Fragmenten enthalten sollte, da sonst, wie Sie wissen, der Speicher anfängt, sich zu " freuen ".

Der Standardansatz für die Interaktion zwischen Fragmenten und ViewModels ist das Abonnieren von LiveData in ViewModel. LiveData möglich, Ereignisse direkt über LiveData zu übertragen, da bei diesem Ansatz nicht berücksichtigt wird, ob das Ereignis bereits abgeschlossen wurde oder nicht.

Welche Lösungen gibt es:


1. Verwenden Sie SingleLiveEvent
Vorteile: Das Event wird einmal ausgeführt.
Nachteile: ein Ereignis - ein SingleLiveEvent . Bei einer großen Anzahl von Ereignissen erscheinen N Ereignisobjekte im ViewModel, von denen jedes im Fragment abonniert werden muss.

2. Ein gutes Beispiel .
Vorteile: Ein Ereignis wird auch einmal ausgeführt. Sie können Daten vom viewModel zum Fragment übertragen.
Nachteile: Daten im Ereignis sind erforderlich, aber wenn Sie ein Ereignis ohne Daten ausführen müssen (val content: T) , müssen Sie eine andere Klasse erstellen. Es löst nicht das Problem, einen Ereignistyp einmal auszuführen (das Ereignis selbst wird einmal ausgeführt, aber dieser Ereignistyp wird so oft ausgeführt, wie wir ihn vom ViewModel aus ausführen). Beispielsweise werden N-Anforderungen asynchron gesendet, es gibt jedoch kein Netzwerk. Jede Anforderung wird mit einem Netzwerkfehler zurückgegeben und ein Fragment von N Ereignissen zu einem Netzwerkfehler abrufen. In dem Fragment werden N Warnungen geöffnet. Der Benutzer wird eine solche Entscheidung nicht genehmigen :). Wir sollten ihm einmal eine Nachricht mit diesem Fehler zeigen. Mit anderen Worten, dieser Ereignistyp sollte einmal ausgeführt werden.

Lösung


Wir nehmen die Idee von SingleLiveEvent auf, um Ereignisbehandlungsinformationen als Basis zu speichern.

Definieren Sie mögliche Ereignistypen


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

Erstellen Sie eine Ereignisbasisklasse - NavigationEvent


isHandled gibt an, ob das Ereignis empfangen wurde (wir glauben, dass es ausgeführt wurde, wenn Observer im Fragment empfangen wurde).

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

Erstelle eine Emitter Klasse - Emitter


Die Event-Emitter-Klasse erbt von LiveData < NavigationEvent >. Diese Klasse wird im ViewModel verwendet, um Ereignisse auszulösen.

 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 - wir müssen verstehen, ob mindestens ein Observer Emitter abonniert hat. Und für den Fall, dass der Abonnent erschienen ist und sich die Ereignisse, die auf ihn warten, angesammelt haben, senden wir diese Ereignisse. Eine wichtige Klarstellung: Das Senden von Ereignissen ist nicht über this.postValue(event) erforderlich, sondern über den Setter this.value = event . Andernfalls erhält der Abonnent nur das letzte Ereignis auf der Liste.

Die Methode newEvent(event, type) zum Senden eines neuen Ereignisses akzeptiert zwei Parameter - nämlich das Ereignis selbst und den Typ dieses Ereignisses.

Um sich nicht an alle Arten von Ereignissen (lange Namen) zu erinnern, erstellen wir separate öffentliche Methoden, die nur das Ereignis selbst akzeptieren:

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

Formal können Sie Emitter im ViewModel abonnieren und Ereignisse unabhängig von ihrer Behandlung empfangen (unabhängig davon, ob das Ereignis bereits verarbeitet wurde oder nicht).

Erstellen Sie eine Event Observer-Klasse - 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) } } } } } } 

Dieser Observer akzeptiert eine Funktion höherer Ordnung als Eingabe - die Ereignisverarbeitung wird in einem Fragment geschrieben (Beispiel unten).

Die Methode clearExecutedEvents() zum Bereinigen von ausgeführten Ereignissen (diejenigen, die einmal ausgeführt werden sollten). Erforderlich beim Aktualisieren des Bildschirms, z. B. in swipeToRefresh() .

Nun, eigentlich die Hauptmethode onChange() , die auftritt, wenn neue onChange() empfangen werden, die dieser Beobachter abonniert.

Wenn das Ereignis eine unbegrenzte Anzahl von Ausführungsarten hat, prüfen wir, ob das Ereignis ausgeführt wurde, und verarbeiten es. Wir führen das Ereignis aus und zeigen an, dass es empfangen und verarbeitet wird.

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

Wenn das Ereignis vom Typ ist, der einmal ausgeführt werden soll, prüfen Sie, ob die Klasse dieses Ereignisses in der Hash-Tabelle enthalten ist. Wenn nicht, führen Sie das Ereignis aus und fügen Sie die Klasse dieses Ereignisses der Hash-Tabelle hinzu.

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

Aber wie überträgt man Daten innerhalb von Ereignissen?


Dazu wird die MyFragmentNavigation Schnittstelle MyFragmentNavigation , die aus Klassen besteht, die von NavigationEvent() geerbt wurden. Im Folgenden werden verschiedene Klassen mit und ohne übergebenen Parametern erstellt.

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

Wie es in der Praxis funktioniert


Senden von Ereignissen aus 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() } } 

Empfangen von Ereignissen in einem Fragment:

 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(){ ... } } 

Im Wesentlichen haben wir eine Analogie von Rx- BehaviorSubjec ta und EventBus -a erhalten, die nur auf LiveData basiert, in der Emitter Ereignisse vor dem Auftreten eines Artist-Observers erfassen und in der Observer die Ereignistypen überwachen und gegebenenfalls nur einmal aufrufen kann.

Willkommen in den Kommentaren mit Vorschlägen.

Link zur Quelle .
Ratenplan von der Home Credit Bank .

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


All Articles