MVIDroid: Überprüfung der neuen MVI-Bibliothek (Model-View-Intent)

Hallo an alle! In diesem Artikel möchte ich über die neue Bibliothek sprechen, die das MVI-Designmuster in Android einführt. Diese Bibliothek heißt MVIDroid, ist zu 100% in der Kotlin-Sprache geschrieben, leichtgewichtig und verwendet RxJava 2.x. Ich bin der Autor der Bibliothek, der Quellcode ist auf GitHub verfügbar und Sie können sie über JitPack (Link zum Repository am Ende des Artikels) verbinden. Dieser Artikel besteht aus zwei Teilen: einer allgemeinen Beschreibung der Bibliothek und einem Verwendungsbeispiel.


MVI


Als Vorwort möchte ich Sie daran erinnern, was MVI ist. Modell - Ansicht - Absicht oder, falls auf Russisch, Modell - Ansicht - Absicht. Dies ist ein Entwurfsmuster, in dem ein Modell eine aktive Komponente ist, die Intents bei der Eingabe annimmt und Zustand erzeugt. View (View) wiederum nimmt View Models (View Model) und erzeugt genau diese Absichten. Der Zustand wird mithilfe einer Transformatorfunktion (View Model Mapper) in ein Ansichtsmodell umgewandelt. Das MVI-Muster kann schematisch wie folgt dargestellt werden:


MVI


Die MVIDroid-Ansicht erzeugt keine Absichten direkt. Stattdessen werden Ansichtsereignisse (UI-Ereignisse) erzeugt, die dann mithilfe einer Transformatorfunktion in Absichten konvertiert werden.


Ansicht


Hauptkomponenten von MVIDroid


Modell


Beginnen wir mit dem Modell. In der Bibliothek wird das Konzept von Model etwas erweitert, hier werden nicht nur Staaten, sondern auch Labels erzeugt. Tags werden verwendet, um die Modelle miteinander zu kommunizieren. Die Labels einiger Modelle können mit Hilfe von Transformationsfunktionen in die Absichten anderer Modelle umgewandelt werden. Das Modell kann schematisch wie folgt dargestellt werden:


Modell


In MVIDroid wird das Modell durch die MviStore-Schnittstelle dargestellt (der Name Store wurde von Redux entliehen):


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 so haben wir:


  • Die Schnittstelle verfügt über drei generische Parameter: State - der State-Typ, Intent - der Intention-Typ und Label - der Label-Typ.
  • Es enthält drei Felder: state - der aktuelle Status des Modells, state - Observable States und Labels - Observable Labels. Die letzten beiden Felder bieten die Möglichkeit, Änderungen im Status bzw. in den Tags zu abonnieren.
  • Absichten der Verbraucher (Verbraucher)
  • Es ist Disposable, wodurch das Modell zerstört und alle darin ablaufenden Prozesse gestoppt werden können.

Beachten Sie, dass alle Model-Methoden im Hauptthread ausgeführt werden müssen. Gleiches gilt für jede andere Komponente. Natürlich können Sie Hintergrundaufgaben mit Standard-RxJava-Werkzeugen ausführen.


Komponente


Eine Komponente in MVIDroid ist eine Gruppe von Modellen, die durch ein gemeinsames Ziel verbunden sind. Beispielsweise können Sie in der Komponente alle Modelle für einen beliebigen Bildschirm auswählen. Mit anderen Worten, die Komponente ist eine Fassade für die darin enthaltenen Modelle und ermöglicht das Ausblenden von Implementierungsdetails (Modelle, Transformationsfunktionen und deren Verknüpfungen). Schauen wir uns das Komponentenschema an:


Komponente


Wie aus dem Diagramm ersichtlich, erfüllt die Komponente die wichtige Funktion der Umwandlung und Umleitung von Ereignissen.


Die vollständige Liste der Komponentenfunktionen lautet wie folgt:


  • Ordnet die eingehenden View-Ereignisse und -Tags jedem Modell mithilfe der bereitgestellten Transformationsfunktionen zu.
  • Zeigt ausgehende Model Tags außerhalb an
  • Zerstört alle Modelle und bricht alle Verbindungen, wenn Component 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 Generic-Parameter: UiEvent - Typ der Ereignisse der Ansicht und Zustände - Typ der Zustände der Modelle
  • Enthält das Zustandsfeld, über das der Zugriff auf die Gruppe "Modellzustände" möglich ist (z. B. als Schnittstelle oder Datenklasse).
  • Consumer (Consumer) Ereignisanzeige
  • Es ist ein Einwegartikel, der die Zerstörung der Komponente und aller ihrer Modelle ermöglicht.

Ansicht


Wie Sie sich leicht vorstellen können, benötigen Sie eine Ansicht, um Daten anzuzeigen. Die Daten für jede Ansicht werden in einem Ansichtsmodell gruppiert und normalerweise als Datenklasse (Kotlin) dargestellt. Betrachten Sie die Präsentationsoberflä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 ist der Typ des View-Modells und UiEvent ist der Typ der View-Ereignisse. Ein uiEvents-Feld ist Observable View Events, mit dem Clients dieselben Ereignisse abonnieren können. Und eine Methode von subscribe (), die die Möglichkeit bietet, das View Model zu abonnieren.


Verwendungsbeispiel


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 vermittelt, wie man all dies anwendet und in welche Richtung es weitergeht. Sei es ein UUID-Generator: Durch Drücken einer Taste erzeugen wir eine UUID und zeigen sie auf dem Bildschirm an.


Einreichung


Zunächst beschreiben wir das Repräsentationsmodell:


data class ViewModel(val text: String)

Und Präsentationsveranstaltungen:


sealed class UiEvent {
    object OnGenerateClick: UiEvent()
}

Jetzt implementieren wir die View selbst, dazu benötigen wir die Vererbung von der abstrakten MviAbstractView-Klasse:


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 Änderungen an der UUID und aktualisieren TextView, wenn wir eine neue UUID erhalten, und per Knopfdruck senden wir das OnGenerateClick-Ereignis.


Modell


Das Modell besteht aus zwei Teilen: der Schnittstelle und der 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 zeigt die Zustandsarten (State) und Intentions (Intent) an. Die Art der Etiketten ist Nichts, da unser Modell sie nicht produziert. Die Schnittstelle enthält auch Klassen von Zuständen und Absichten.


Um das Modell zu implementieren, müssen Sie wissen, wie es funktioniert. Absichten (Absichten) werden in das Modell eingegeben, die mit der speziellen IntentToAction-Funktion in Aktionen (Aktion) umgewandelt werden. Aktionen kommen zur Eingabe an den Executor, der sie ausführt und Ergebnisse (Ergebnis) und Labels erzeugt. Die Ergebnisse werden dann an einen Reducer übertragen, der den aktuellen Status in einen neuen konvertiert.


Alle vier Markenmodelle:


  • IntentToAction - eine Funktion, die Absichten in Aktionen umwandelt
  • MviExecutor - führt Aktionen aus und erzeugt Ergebnisse und Labels
  • MviReducer - Konvertiert Paare (Zustand, Ergebnis) in neue Zustände
  • 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. Bootstrapper wird beim Erstellen eines Modells automatisch gestartet.

Um das Modell selbst zu erstellen, muss eine spezielle Factory of Models verwendet werden. Es wird durch die MviStoreFactory-Schnittstelle und deren Implementierung MviDefaultStoreFactory dargestellt. Das Werk übernimmt die konstituierenden Modelle und liefert ein gebrauchsfertiges Modell.


Die Fabrik unseres Modells sieht folgendermaßen 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. Erstellen Sie zuerst eine Factory-Methode, dann Aktionen und Ergebnisse, gefolgt vom Künstler und ganz am Ende des Reduzierers.


Komponente


Die Zustände der Komponente (Gruppe "Staaten") werden durch die Datenklasse beschrieben:


data class States(val uuidStates: Observable<UuidStore.State>)

Beim Hinzufügen neuer Modelle zu einer Komponente sollten deren Zustände ebenfalls der Gruppe hinzugefügt werden.


Und eigentlich 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 Typen von Status und Ansichtsereignissen angegeben, unser Modell an die Oberklasse übergeben und das Statusfeld implementiert. Darüber hinaus haben wir eine Transformationsfunktion erstellt, die View Events in die Absicht unseres Modells transformiert.


Darstellung von Modelldarstellungen


Wir haben einen Zustand und ein Modell der Darstellung, es ist Zeit, einen in den anderen 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")
        }
}

Kommunikation (Bindung)


Die Verfügbarkeit der Komponente und der Ansicht allein reicht nicht aus. Damit alles funktionieren kann, 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 akzeptiert. Diese Methode ist eine Erweiterungsmethode über LifecycleOwner (dh Aktivität und Fragment) und verwendet den DefaultLifecycleObserver aus dem Arch-Paket, für den eine Kompatibilität mit Java 8-Quellen erforderlich ist. Wenn Sie aus irgendeinem Grund Java 8 nicht verwenden können, ist die zweite bind () -Methode für Sie geeignet, die keine Erweiterungsmethode ist und MviLifecyleObserver zurückgibt. In diesem Fall müssen Sie die Lebenszyklusmethoden selbst aufrufen.


Links


Der Quellcode der Bibliothek sowie detaillierte Anweisungen zum Verbinden und Verwenden der Bibliothek finden Sie auf GitHub .


Jetzt auch beliebt: