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