Hallo allerseits! In diesem Artikel möchte ich über eine neue Bibliothek sprechen, die das MVI-Entwurfsmuster auf Android bringt. Diese Bibliothek heißt MVIDroid, ist zu 100% in Kotlin geschrieben, leichtgewichtig und verwendet RxJava 2.x. Ich persönlich bin der Autor der Bibliothek, ihr Quellcode ist auf GitHub verfügbar und Sie können ihn über JitPack verbinden (Link zum Repository am Ende des Artikels). Dieser Artikel besteht aus zwei Teilen: einer allgemeinen Beschreibung der Bibliothek und einem Beispiel für ihre Verwendung.
MVI
Lassen Sie mich als Vorwort daran erinnern, was MVI ist. Model - View - Intent oder, wenn auf Russisch, Model - View - Intention. Dies ist ein solches Entwurfsmuster, bei dem das Modell eine aktive Komponente ist, die Absichten akzeptiert und Status generiert. Präsentation (Ansicht) akzeptiert wiederum Repräsentationsmodelle (Ansichtsmodell) und erzeugt dieselben Absichten. Der Status wird mithilfe einer Transformationsfunktion (View Model Mapper) in ein Ansichtsmodell konvertiert. Schematisch kann das MVI-Muster wie folgt dargestellt werden:

In MVIDroid erzeugt die Darstellung keine Absichten direkt. Stattdessen werden UI-Ereignisse erzeugt, die dann mithilfe einer Transformationsfunktion in Intent konvertiert werden.

Hauptkomponenten von MVIDroid
Modell
Beginnen wir mit dem Modell. In der Bibliothek wird das Konzept des Modells leicht erweitert, hier werden nicht nur Zustände, sondern auch Beschriftungen erzeugt. Beschriftungen werden verwendet, um Modelle miteinander zu kommunizieren. Beschriftungen einiger Modelle können mithilfe von Transformationsfunktionen in die Absichten anderer Modelle konvertiert werden. Schematisch kann das Modell wie folgt dargestellt werden:

In MVIDroid wird das Modell durch die MviStore-Schnittstelle dargestellt (der Store-Name ist von Redux entlehnt):
interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Und damit wir haben:
- Die Schnittstelle verfügt über drei allgemeine Parameter: Status - Typ des Status, Absicht - Typ der Absicht und Bezeichnung - Typ der Beschriftungen
- Es enthält drei Felder: Status - Der aktuelle Status des Modells, Status - Beobachtbare Zustände und Beschriftungen - Beobachtbare Beschriftungen. Die letzten beiden Felder ermöglichen es, Änderungen am Status bzw. an Tags zu abonnieren.
- Verbraucherabsicht
- Es ist verfügbar, wodurch es möglich ist, das Modell zu zerstören und alle darin ablaufenden Prozesse zu stoppen
Beachten Sie, dass alle Model-Methoden im Hauptthread ausgeführt werden müssen. Gleiches gilt für alle anderen Komponenten. Natürlich können Sie Hintergrundaufgaben mit Standard-RxJava-Tools ausführen.
Komponente
Eine Komponente in MVIDroid ist eine Gruppe von Modellen, die durch ein gemeinsames Ziel verbunden sind. Sie können beispielsweise alle Modelle für einen Bildschirm in der Komponente auswählen. Mit anderen Worten, die Komponente ist die Fassade für die darin enthaltenen Modelle und ermöglicht es Ihnen, Implementierungsdetails (Modelle, Transformationsfunktionen und ihre Beziehungen) auszublenden. Schauen wir uns das Komponentendiagramm an:

Wie Sie dem Diagramm entnehmen können, hat die Komponente eine wichtige Funktion zum Transformieren und Umleiten von Ereignissen.
Eine vollständige Liste der Komponentenfunktion lautet wie folgt:
- Ordnet jedem Modell eingehende Repräsentationsereignisse und Tags mithilfe der bereitgestellten Transformationsfunktionen zu
- Bringen Sie ausgehende Modelletiketten nach außen
- Zerstört alle Modelle und unterbricht alle Verbindungen, wenn eine Komponente zerstört wird
Die Komponente hat auch eine eigene Schnittstelle:
interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Betrachten Sie die Komponentenschnittstelle genauer:
- Enthält zwei generische Parameter: UiEvent - Typ der Ansichtsereignisse und Status - Typ der Status von Modellen
- Enthält das Feld "Zustände", das den Zugriff auf die Gruppe "Modellzustände" ermöglicht (z. B. als Schnittstelle oder Datenklasse).
- Consumer View-Ereignisse
- Es ist Einweg, wodurch die Komponente und alle ihre Modelle zerstört werden können
Anzeigen
Wie Sie vielleicht erraten haben, ist eine Ansicht erforderlich, um Daten anzuzeigen. Die Daten für jede Ansicht sind in einem Ansichtsmodell zusammengefasst und werden normalerweise als Datenklasse (Kotlin) dargestellt. Betrachten Sie die Ansichtsoberfläche:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
Hier ist alles etwas einfacher. Zwei generische Parameter: ViewModel - Typ des Ansichtsmodells und UiEvent - Typ der Ansichtsereignisse. Ein uiEvents-Feld ist das Observable View-Ereignis, mit dem Kunden dieselben Ereignisse abonnieren können. Und eine subscribe () -Methode, mit der Sie View Models abonnieren können.
Anwendungsbeispiel
Jetzt ist es an der Zeit, etwas in der Praxis auszuprobieren. Ich schlage vor, etwas sehr Einfaches zu tun. Etwas, das nicht viel Mühe erfordert, um es zu verstehen, und gleichzeitig eine Vorstellung davon gibt, wie man all dies nutzt und in welche Richtung man vorankommt. Sei es ein UUID-Generator: Auf Knopfdruck generieren wir eine UUID und zeigen sie auf dem Bildschirm an.
Einreichung
Zunächst beschreiben wir das Ansichtsmodell:
data class ViewModel(val text: String)
Und Ereignisse anzeigen:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
Jetzt implementieren wir die Ansicht selbst. Dazu benötigen wir die Vererbung von der abstrakten Klasse MviAbstractView:
class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } }
Alles ist sehr einfach: Wir abonnieren UUID-Änderungen und aktualisieren TextView, wenn wir eine neue UUID erhalten. Wenn Sie auf die Schaltfläche klicken, senden wir das OnGenerateClick-Ereignis.
Modell
Das Modell besteht aus zwei Teilen: Schnittstelle und Implementierung.
Schnittstelle:
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
Hier ist alles einfach: Unsere Schnittstelle erweitert die MviStore-Schnittstelle und gibt die Typen State (State) und Intent (Intent) an. Art der Tags - Nichts, da unser Modell sie nicht produziert. Die Schnittstelle enthält auch die Klassen State und Intent.
Um das Modell zu implementieren, müssen Sie verstehen, wie es funktioniert. Am Eingang des Modells werden Absichten empfangen, die mit der speziellen IntentToAction-Funktion in Aktionen umgewandelt werden. Aktionen werden in den Executor eingegeben, der sie ausführt und das Ergebnis und die Bezeichnung erstellt. Die Ergebnisse gehen dann an den Reduzierer, der den aktuellen Status in einen neuen konvertiert.
Alle vier komponierenden Modelle:
- IntentToAction - eine Funktion, die Intent in Action konvertiert
- MviExecutor - Führt Aktionen aus und erzeugt Ergebnisse und Tags
- MviReducer - konvertiert Paare (Status, Ergebnis) in neue Status
- MviBootstrapper ist eine spezielle Komponente, mit der Sie das Modell initialisieren können. Gibt alle Aktionen aus, die auch an den Executor gehen. Sie können eine einmalige Aktion ausführen oder eine Datenquelle abonnieren und Aktionen für bestimmte Ereignisse ausführen. Der Bootstrapper wird automatisch gestartet, wenn Sie ein Modell erstellen.
Um das Modell selbst zu erstellen, müssen Sie eine spezielle Factory von Modellen verwenden. Es wird durch die MviStoreFactory-Schnittstelle und deren Implementierung von MviDefaultStoreFactory dargestellt. Die Fabrik akzeptiert die Komponenten des Modells und stellt ein gebrauchsfertiges Modell aus.
Die Fabrik unseres Modells sieht wie folgt aus:
class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }
Dieses Beispiel zeigt alle vier Komponenten des Modells. Zuerst die Factory-Erstellungsmethode, dann Aktionen und Ergebnisse, gefolgt vom Auftragnehmer und ganz am Ende vom Reduzierer.
Komponente
Die Komponentenzustände (Statusgruppe) werden durch die Datenklasse beschrieben:
data class States(val uuidStates: Observable<UuidStore.State>)
Wenn Sie einer Komponente neue Modelle hinzufügen, sollte deren Status ebenfalls zur Gruppe hinzugefügt werden.
Und in der Tat die Implementierung selbst:
class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }
Wir haben die abstrakte Klasse MviAbstractComponent geerbt, die Arten von Status und Ansichtsereignissen angegeben, unser Modell an die Superklasse übergeben und das Statusfeld implementiert. Darüber hinaus haben wir eine Transformationsfunktion erstellt, die Ansichtsereignisse in Absichten unseres Modells umwandelt.
Zuordnungsansichtsmodelle
Wir haben Bedingungen und Präsentationsmodelle, es ist Zeit, eine in eine andere umzuwandeln. Dazu implementieren wir die MviViewModelMapper-Schnittstelle:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
Bindung
Das Vorhandensein der Komponente und der Präsentation allein reicht nicht aus. Damit alles funktioniert, müssen sie verbunden sein. Es ist Zeit, eine Aktivität zu erstellen:
class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } }
Wir haben die bind () -Methode verwendet, die eine Komponente und ein Array von Ansichten mit den Mappern ihrer Modelle verwendet. Diese Methode ist eine Erweiterungsmethode über LifecycleOwner (Aktivität und Fragment) und verwendet den DefaultLifecycleObserver aus dem Arch-Paket, für den Java 8-Quellkompatibilität erforderlich ist. Wenn Sie Java 8 aus irgendeinem Grund nicht verwenden können, ist die zweite bind () -Methode für Sie geeignet. Dies ist keine Erweiterungsmethode und gibt MviLifecyleObserver zurück. In diesem Fall müssen Sie die Lebenszyklusmethoden selbst aufrufen.
Referenzen
Der Quellcode der Bibliothek sowie detaillierte Anweisungen zum Verbinden und Verwenden finden Sie auf GitHub .