MVI-Architekturvorlage in Kotlin Multiplattform, Teil 1



Vor ungefĂ€hr einem Jahr interessierte ich mich fĂŒr die neue Kotlin Multiplatform-Technologie. Sie können gemeinsamen Code schreiben und fĂŒr verschiedene Plattformen kompilieren, wĂ€hrend Sie auf deren API zugreifen können. Seitdem experimentiere ich aktiv in diesem Bereich und bewerbe dieses Tool in unserem Unternehmen. Ein Ergebnis ist beispielsweise unsere Reaktive Bibliothek - Reaktive Erweiterungen fĂŒr Kotlin Multiplatform.

In Badoo- und Bumble-Anwendungen fĂŒr die Entwicklung fĂŒr Android verwenden wir die MVI-Architekturvorlage (weitere Informationen zu unserer Architektur finden Sie im Artikel von Zsolt Kocsi: „ Moderne Kotlin-basierte MVI-Architektur"). Bei der Arbeit an verschiedenen Projekten wurde ich ein großer Fan dieses Ansatzes. NatĂŒrlich durfte ich die Gelegenheit nicht verpassen, MVI in Kotlin Multiplatform auszuprobieren. DarĂŒber hinaus war der Fall geeignet: Wir mussten Beispiele fĂŒr die Reaktive Bibliothek schreiben. Nach meinen Experimenten war ich noch mehr von MVI inspiriert.

Ich achte immer darauf, wie Entwickler Kotlin Multiplatform verwenden und wie sie die Architektur solcher Projekte erstellen. Nach meinen Beobachtungen ist der durchschnittliche Kotlin Multiplatform-Entwickler tatsĂ€chlich ein Android-Entwickler, der die MVVM-Vorlage in seiner Arbeit verwendet, nur weil er so daran gewöhnt ist. Einige wenden zusĂ€tzlich „saubere Architektur“ an. Meiner Meinung nach ist MVI jedoch am besten fĂŒr Kotlin Multiplatform geeignet, und „saubere Architektur“ ist eine unnötige Komplikation.

Aus diesem Grund habe ich beschlossen, diese Serie von drei Artikeln zu folgenden Themen zu schreiben:

  1. Eine kurze Beschreibung der MVI-Vorlage, eine ErklÀrung des Problems und die Erstellung eines gemeinsamen Moduls mit Kotlin Multiplatform.
  2. Integration eines gemeinsamen Moduls in iOS- und Android-Anwendungen.
  3. Unit- und Integrationstests.

Unten ist der erste Artikel in der Reihe. Es ist fĂŒr alle von Interesse, die Kotlin Multiplatform bereits verwenden oder nur planen.

Ich stelle sofort fest, dass der Zweck dieses Artikels nicht darin besteht, Ihnen das Arbeiten mit der Kotlin-Multiplattform selbst beizubringen. Wenn Sie der Meinung sind, dass Sie in diesem Bereich nicht ĂŒber ausreichende Kenntnisse verfĂŒgen, sollten Sie sich zunĂ€chst mit der EinfĂŒhrung und Dokumentation vertraut machen (insbesondere mit den Abschnitten „ ParallelitĂ€t “ und „ UnverĂ€nderlichkeit “, um die Funktionen des Kotlin / Native-Speichermodells zu verstehen). In diesem Artikel werde ich nicht die Konfiguration des Projekts, der Module und anderer Dinge beschreiben, die nicht mit dem Thema zusammenhĂ€ngen.

MVI


Erinnern wir uns zunĂ€chst daran, was MVI ist. Die AbkĂŒrzung steht fĂŒr Model-View-Intent. Es gibt nur zwei Hauptkomponenten im System:

  • Modell - eine Schicht aus Logik und Daten (das Modell speichert auch den aktuellen Status des Systems);
  • (View) — UI-, (states) (intents).

Das folgende Diagramm ist wahrscheinlich vielen bereits bekannt:



Wir sehen also diese sehr grundlegenden Komponenten: das Modell und die PrÀsentation. Alles andere sind die Daten, die zwischen ihnen zirkulieren.

Es ist leicht zu erkennen, dass sich Daten nur in eine Richtung bewegen. ZustÀnde kommen aus dem Modell und fallen zur Anzeige in die Ansicht, Absichten kommen aus der Ansicht und gehen zur Verarbeitung in das Modell. Diese Zirkulation wird als unidirektionaler Datenfluss bezeichnet.

In der Praxis wird ein Modell hĂ€ufig durch eine EntitĂ€t namens Store dargestellt (es wird von Redux ausgeliehen). Dies ist jedoch nicht immer der Fall. In unserer MVICore- Bibliothek heißt das Modell beispielsweise Feature.

Es ist auch erwĂ€hnenswert, dass MVI sehr eng mit der ReaktivitĂ€t zusammenhĂ€ngt. Die Darstellung von Datenströmen und deren Transformation sowie die Verwaltung des Lebenszyklus von Abonnements ist sehr praktisch, wenn reaktive Programmierbibliotheken verwendet werden. Eine relativ große Anzahl von ihnen ist jetzt verfĂŒgbar. Wenn Sie jedoch allgemeinen Code in Kotlin Multiplatform schreiben, können Sie nur Bibliotheken mit mehreren Plattformen verwenden. Wir brauchen Abstraktion fĂŒr Datenströme, wir brauchen die FĂ€higkeit, ihre Ein- und AusgĂ€nge zu verbinden und zu trennen sowie Transformationen durchzufĂŒhren. Im Moment kenne ich zwei solcher Bibliotheken:

  • unsere Reaktive Bibliothek - Implementierung von Reactive Extensions auf Kotlin Multiplatform;
  • Coroutinen und Flow - Implementierung von Cold Streams mit Kotlin Coroutinen.

Formulierung des Problems


In diesem Artikel wird gezeigt, wie die MVI-Vorlage in Kotlin Multiplatform verwendet wird und welche Vor- und Nachteile dieser Ansatz hat. Daher werde ich nicht an eine bestimmte MVI-Implementierung gebunden sein. Ich werde jedoch Reaktive verwenden, da noch Datenströme benötigt werden. Wenn gewĂŒnscht, kann Reaktive nach Kenntnis der Idee durch Coroutinen und Flow ersetzt werden. Im Allgemeinen werde ich versuchen, unser MVI so einfach wie möglich zu gestalten, ohne unnötige Komplikationen.

Um MVI zu demonstrieren, werde ich versuchen, das einfachste Projekt zu implementieren, das die folgenden Anforderungen erfĂŒllt:

  • UnterstĂŒtzung fĂŒr Android und iOS;
  • Demonstration des asynchronen Betriebs (Eingabe-Ausgabe, Datenverarbeitung usw.);
  • so viel allgemeiner Code wie möglich;
  • UI-Implementierung mit nativen Tools jeder Plattform;
  • Mangel an Rx auf der Plattformseite (so dass Sie keine AbhĂ€ngigkeiten von Rx als "API" angeben mĂŒssen).

Als Beispiel habe ich eine sehr einfache Anwendung ausgewĂ€hlt: einen Bildschirm mit einer SchaltflĂ€che, indem ich darauf klicke, auf den eine Liste mit beliebigen Bildern von Katzen heruntergeladen und angezeigt wird. Zum Hochladen von Bildern verwende ich die offene API: https://thecatapi.com . Auf diese Weise können Sie die Anforderungen des asynchronen Betriebs erfĂŒllen, da Sie Listen aus dem Web herunterladen und die JSON-Datei analysieren mĂŒssen.

Den gesamten Quellcode des Projekts finden Sie auf unserem GitHub .

Erste Schritte: Abstraktionen fĂŒr MVI


Zuerst mĂŒssen wir einige Abstraktionen fĂŒr unser MVI einfĂŒhren. Wir benötigen die grundlegenden Komponenten - das Modell und die Ansicht - und einige Typealien.

Typealiasen


Um Absichten zu verarbeiten, fĂŒhren wir einen Akteur (Actor) ein - eine Funktion, die die Absicht und den aktuellen Status akzeptiert und einen Strom von Ergebnissen zurĂŒckgibt (Effekt):

typealias Akteur < Zustand , Absicht , Wirkung > = ( Zustand , Absicht ) - > Beobachtbar < Effekt >
Ansicht roh MviKmpActor.kt gehostet mit ❀ von GitHub

Wir brauchen auch einen Reduzierer (Reduzierer) - eine Funktion, die wirksam wird und den aktuellen Status und einen neuen Status zurĂŒckgibt:

typealias Reducer < State , Effect > = ( State , Effect ) - > State
Ansicht roh MviKmpReducer.kt gehostet mit ❀ von GitHub

GeschÀft


Store prÀsentiert ein Modell von MVI. Er muss Absichten akzeptieren und einen Strom von Staaten abgeben. Beim Abonnieren eines Statusstroms sollte der aktuelle Status ausgegeben werden.

Lassen Sie uns die entsprechende Schnittstelle vorstellen:

Schnittstelle Shop < in Intent : Jeder , aus Staat : Any > : Consumer < Intent >, Observable < Staat >, Einweg
Ansicht roh MviKmpStoreInterface.kt gehostet mit ❀ von GitHub

Unser Shop hat also folgende Eigenschaften:

  • hat zwei generische Parameter: Eingabeabsicht und Ausgabestatus;
  • ist ein Verbraucher von Absichten (Verbraucher <Intent>);
  • ist ein Statusstrom (Observable <State>);
  • es ist zerstörbar (Einweg).

Da es nicht sehr bequem ist, eine solche Schnittstelle jedes Mal zu implementieren, benötigen wir einen bestimmten Assistenten:

Klasse StoreHelper < in Intent : Any , out State : Any , in Effect : Any > (
initialState : State ,
Privat val Schauspieler : Schauspieler < Staat , Intent , Effect >,
private val reducer: Reducer<State, Effect>
) : Observable<State>, DisposableScope by DisposableScope() {
init {
ensureNeverFrozen()
}
private val subject = BehaviorSubject(initialState)
fun onIntent(intent: Intent) {
actor(subject.value, intent).subscribeScoped(isThreadLocal = true, onNext = ::onEffect)
}
fun onEffect ( Effekt : Effekt ) {
subject.onNext (Reduzierer (subject.value, effect))
}}
Überschreibung Spaß abonnieren ( Beobachter : ObservableObserver < Staat >) {
subject.subscribe (Beobachter)
}}
}}
Ansicht roh MviKmpStoreHelper.kt gehostet mit ❀ von GitHub


StoreHelper ist eine kleine Klasse, die es uns einfacher macht, einen Store zu erstellen. Es hat die folgenden Eigenschaften:

  • hat drei generische Parameter: Eingabeabsicht und Wirkung und Ausgabestatus;
  • akzeptiert den Ausgangszustand durch den Konstrukteur, den Akteur und das Getriebe;
  • ist ein Strom von Staaten;
  • zerstörbar (Einweg);
  • nicht einfrieren (damit Abonnenten auch nicht eingefroren werden );
  • implementiert DisposableScope (Schnittstelle von Reaktive zur Verwaltung von Abonnements);
  • akzeptiert und verarbeitet Absichten und Wirkungen.

Schauen Sie sich das Diagramm unseres Shops an. Der Darsteller und das Getriebe sind Implementierungsdetails:



Betrachten wir die onIntent-Methode genauer:

  • akzeptiert Absichten als Argument;
  • ruft den Schauspieler an und gibt die Absicht und den aktuellen Zustand an ihn weiter;
  • abonniert den vom Schauspieler zurĂŒckgegebenen Effektstrom;
  • Leitet alle Effekte auf die onEffect-Methode.
  • Das Abonnieren von Effekten erfolgt mit dem Flag isThreadLocal (dies vermeidet das Einfrieren in Kotlin / Native).

Schauen wir uns nun die onEffect-Methode genauer an:

  • nimmt Effekte als Argument;
  • ruft das Getriebe auf und ĂŒbertrĂ€gt den Effekt und den aktuellen Zustand in das Getriebe;
  • Übergibt den neuen Status an das BehaviorSubject, wodurch alle Abonnenten den neuen Status erhalten.

Aussicht


Kommen wir nun zur PrĂ€sentation. Es sollte Modelle fĂŒr die Anzeige akzeptieren und einen Strom von Ereignissen ausgeben. Wir werden auch eine separate Schnittstelle erstellen:

Schnittstelle MviView < in Modell : Beliebig , aus Ereignis : Beliebig > {
val events : Beobachtbares < Ereignis >
Spaß rendern ( Modell : Modell )
}}
Ansicht roh MviKmpMviView.kt gehostet mit ❀ von GitHub


Eine Ansicht hat die folgenden Eigenschaften:

  • hat zwei generische Parameter: Eingabemodell und Ausgabeereignis;
  • akzeptiert Modelle zur Anzeige mit der Rendermethode;
  • Versendet einen Ereignisstrom mithilfe der Eigenschaft events.

Ich habe dem MviView-Namen das Mvi-PrĂ€fix hinzugefĂŒgt, um Verwechslungen mit Android View zu vermeiden. Außerdem habe ich die Schnittstellen Consumer und Observable nicht erweitert, sondern lediglich die Eigenschaft und die Methode verwendet. Auf diese Weise können Sie die PrĂ€sentationsoberflĂ€che auf eine Plattform fĂŒr die Implementierung (Android oder iOS) einstellen, ohne Rx als API-AbhĂ€ngigkeit zu exportieren. Der Trick besteht darin, dass Clients nicht direkt mit der Eigenschaft "events" interagieren, sondern die MviView-Schnittstelle implementieren und die abstrakte Klasse erweitern.

FĂŒgen Sie diese abstrakte Klasse sofort hinzu, um Folgendes darzustellen:

abstract class AbstractMviView<in Model : Any, Event : Any> : MviView<Model, Event> {
private val subject = PublishSubject<Event>()
override val events: Observable<Event> = subject
protected fun dispatch(event: Event) {
subject.onNext(event)
}
}
view raw MviKmpAbstractMviView.kt gehostet mit ❀ von GitHub

Diese Klasse hilft uns bei der Ausgabe von Ereignissen und bewahrt die Plattform vor der Interaktion mit Rx.

Hier ist ein Diagramm, das zeigt, wie dies funktioniert: Der



Store erzeugt ZustÀnde, die in Modelle umgewandelt und von der Ansicht angezeigt werden. Letzteres erzeugt Ereignisse, die in Absichten konvertiert und zur Verarbeitung an den Store gesendet werden. Dieser Ansatz beseitigt die KohÀrenz zwischen Store und PrÀsentation. In einfachen FÀllen kann eine Ansicht jedoch direkt mit ZustÀnden und Absichten arbeiten.

Das ist alles, was wir brauchen, um MVI zu implementieren. Schreiben wir den allgemeinen Code.

Gemeinsamer Code


Planen


  1. Wir werden ein allgemeines Modul erstellen, dessen Aufgabe es ist, eine Liste von Bildern von Katzen herunterzuladen und anzuzeigen.
  2. UI Wir abstrahieren die Schnittstelle und werden ihre Implementierung nach außen ĂŒbertragen.
  3. Wir werden unsere Implementierung hinter einer praktischen Fassade verstecken.

Kittenstore


Beginnen wir mit der Hauptsache - erstellen Sie einen KittenStore, der eine Liste von Bildern lÀdt:

interne Schnittstelle KittenStore : Store < Intent , State > {
versiegelte Klasse Absicht {
Objekt neu laden : Absicht ()
}}
Datenklasse Staat (
val isLoading : Boolean = false ,
val data : Data = Data . Bilder ()
) {
versiegelte Klasse Daten {
Daten Klasse Bilder ( val Urls : Liste < String > = emptyList ()) : Daten ()
Objekt Fehler : Daten ()
}}
}}
}}


Wir haben die Store-OberflÀche um Absichtstypen und Statustypen erweitert. Bitte beachten Sie: Die Schnittstelle ist als intern deklariert. Unser KittenStore enthÀlt die Implementierungsdetails des Moduls. Unsere Absicht ist nur eine - Neu laden, es bewirkt das Laden einer Liste von Bildern. Aber die Bedingung ist es wert, genauer betrachtet zu werden:

  • Das isLoading-Flag zeigt an, ob der Download gerade lĂ€uft oder nicht.
  • Die Dateneigenschaft kann eine von zwei Optionen annehmen:
    • Bilder - eine Liste von Links zu Bildern;
    • Fehler - bedeutet, dass ein Fehler aufgetreten ist.

Beginnen wir nun mit der Implementierung. Wir werden dies schrittweise tun. Erstellen Sie zunÀchst eine leere KittenStoreImpl-Klasse, die die KittenStore-Schnittstelle implementiert:

interne Klasse KittenStoreImpl (
) : KittenStore , DisposableScope von DisposableScope () {
Spaß ĂŒberschreiben onNext ( Wert : Intent ) {
}}
Überschreibung Spaß abonnieren ( Beobachter : ObservableObserver < Staat >) {
}}
}}


Wir haben auch die bekannte DisposableScope-Schnittstelle implementiert. Dies ist fĂŒr eine bequeme Abonnementverwaltung erforderlich.

Wir mĂŒssen die Liste der Bilder aus dem Web herunterladen und die JSON-Datei analysieren. Deklarieren Sie die entsprechenden AbhĂ€ngigkeiten:

interne Klasse KittenStoreImpl (
privates val Netzwerk : Netzwerk ,
der private val parser : Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
Fun Parse ( json : String ) : Vielleicht < List < String >>
}}
}}

Das Netzwerk lĂ€dt den Text fĂŒr die JSON-Datei aus dem Netzwerk herunter, und Parser analysiert die JSON-Datei und gibt eine Liste der Bildlinks zurĂŒck. Im Fehlerfall endet Vielleicht einfach ohne Ergebnis. In diesem Artikel interessiert uns die Art des Fehlers nicht.

ErklÀren Sie nun die Effekte und den Reduzierer:

interne Klasse KittenStoreImpl (
privates val Netzwerk : Netzwerk ,
der private val parser : Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reduce(state: State, effect: Effect): State =
when (effect) {
is Effect.LoadingStarted -> state.copy(isLoading = true)
is Effect.LoadingFinished -> state.copy(isLoading = false, data = State.Data.Images(urls = effect.imageUrls))
is Effect.LoadingFailed -> state.copy(isLoading = false, data = State.Data.Error)
}
private sealed class Effect {
object LoadingStarted : Effect()
data class LoadingFinished(val imageUrls: List<String>) : Effect()
object LoadingFailed : Effect()
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
Fun Parse ( json : String ) : Vielleicht < List < String >>
}}
}}

Bevor wir mit dem Download beginnen, geben wir den LoadingStarted-Effekt aus, wodurch das isLoading-Flag gesetzt wird. Nach Abschluss des Downloads geben wir entweder LoadingFinished oder LoadingFailed aus. Im ersten Fall löschen wir das Flag isLoading und wenden die Liste der Bilder an, im zweiten Fall löschen wir auch das Flag und wenden den Fehlerstatus an. Bitte beachten Sie, dass Effekte die private API unseres KittenStore sind.

Jetzt implementieren wir den Download selbst:

interne Klasse KittenStoreImpl (
private val network: Network,
private val parser: Parser
) : KittenStore, DisposableScope by DisposableScope() {
override fun onNext(value: Intent) {
}
override fun subscribe(observer: ObservableObserver<State>) {
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
network
.load()
.flatMap(parser::parse)
.map(Effect::LoadingFinished)
.observeOn(mainScheduler)
.asObservable()
.defaultIfEmpty(Effect.LoadingFailed)
.startWithValue(Effect.LoadingStarted)
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
fun load(): Maybe<String>
}
interface Parser {
fun parse(json: String): Maybe<List<String>>
}
}
view raw MviKmpKittenStoreImpl4.kt gehostet mit ❀ von GitHub

Hier ist zu beachten, dass wir Network und Parser an die Reload-Funktion ĂŒbergeben haben, obwohl sie uns bereits als Eigenschaften des Konstruktors zur VerfĂŒgung stehen. Dies geschieht, um Verweise darauf zu vermeiden und infolgedessen den gesamten KittenStore einzufrieren.

Verwenden Sie zum Schluss StoreHelper und beenden Sie die Implementierung von KittenStore:

interne Klasse KittenStoreImpl (
privates val Netzwerk : Netzwerk ,
der private val parser : Parser
) : KittenStore , DisposableScope von DisposableScope () {
private val helper = StoreHelper(State(), ::handleIntent, ::reduce).scope()
override fun onNext(value: Intent) {
helper.onIntent(value)
}
override fun subscribe(observer: ObservableObserver<State>) {
helper.subscribe(observer)
}
private fun handleIntent(state: State, intent: Intent): Observable<Effect> =
when (intent) {
is Intent.Reload -> reload(network, parser)
}
private fun reload(network: Network, parser: Parser): Observable<Effect> =
// Omitted code
private fun reduce(state: State, effect: Effect): State =
// Omitted code
private sealed class Effect {
// Omitted code
}
interface Network {
fun load () : Vielleicht < String >
}}
Schnittstelle Parser {
Fun Parse ( json : String ) : Vielleicht < List < String >>
}}
}}

Unser KittenStore ist fertig! Wir gehen zur PrĂ€sentation ĂŒber.

Kittenview


Deklarieren Sie die folgende Schnittstelle:

Schnittstelle KittenView : MviView < Modell , Ereignis > {
Daten Klasse Modell (
val isLoading : Boolean ,
val isError : Boolean ,
val imageUrls : List < String >
)
versiegelte Klasse Event {
Objekt RefreshTriggered : Event ()
}}
}}

Wir haben ein Ansichtsmodell mit Lade- und Fehlerflags und einer Liste von Bildlinks angekĂŒndigt. Wir haben nur ein Ereignis - RefreshTriggered. Sie wird jedes Mal ausgegeben, wenn der Benutzer ein Update aufruft. KittenView ist die öffentliche API unseres Moduls.

KittenDataSource


Die Aufgabe dieser Datenquelle besteht darin, Text fĂŒr eine JSON-Datei aus dem Web herunterzuladen. Deklarieren Sie wie gewohnt die Schnittstelle:

interne Schnittstelle KittenDataSource {
Spaß Last ( Limit : Int , Offset : Int ) : Vielleicht < String >
}}

Datenquellenimplementierungen werden fĂŒr jede Plattform separat durchgefĂŒhrt. Daher können wir eine Factory-Methode mit Expect / Actual deklarieren:

intern erwarten Spaß KittenDataSource () : KittenDataSource

Die Implementierung der Datenquelle wird im nĂ€chsten Teil erlĂ€utert, in dem Anwendungen fĂŒr iOS und Android implementiert werden.

Integration


Die letzte Phase ist die Integration aller Komponenten.

Implementierung der Netzwerkschnittstelle:

interne Klasse KittenStoreNetwork (
private val dataSource : KittenDataSource
) : KittenStoreImpl . Netzwerk {
Fun Load () ĂŒberschreiben : Möglicherweise < String > = dataSource.load (Limit = 50 , Offset = 0 )
}}
Ansicht roh KittenStoreNetwork.kt gehostet mit ❀ von GitHub


Implementierung der Parser-Schnittstelle:

internes Objekt KittenStoreParser : KittenStoreImpl . Parser {
Überschreibung Spaß Parse ( json : String ) : Vielleicht < Liste < String >> =
vielleichtFromFunction {
Json ( JsonConfiguration . Stabil )
.parseJson (json)
.jsonArray
.map {it.jsonObject.getPrimitive ( " url " ) .content}
}}
.subscribeOn (computationScheduler)
.onErrorComplete ()
}}
Ansicht roh KittenStoreParser.kt gehostet mit ❀ von GitHub

Hier haben wir die Bibliothek kotlinx.serialization verwendet . Das Parsen wird in einem Berechnungsplaner durchgefĂŒhrt, um ein Blockieren des Hauptthreads zu vermeiden.

Status in Ansichtsmodell konvertieren:

interner Spaßzustand . toModel () : Model =
Modell (
isLoading = isLoading,
isError = when ( data ) {
ist Staat . Die Daten . Bilder - > falsch
ist Staat . Die Daten . Fehler - > wahr
},
imageUrls = when ( data ) {
ist Staat . Die Daten . Bilder - > Daten .urls
ist Staat . Die Daten . Fehler - > emptyList ()
}}
)
Ansicht roh MviKmpStateToModel.kt gehostet mit ❀ von GitHub

Ereignisse in Absichten umwandeln:

internes Spaß - Ereignis. toIntent () : Intent =
wenn ( dies ) {
ist Ereignis . RefreshTriggered - > Intent . Neu laden
}}
Ansicht roh MviKmpEventToIntent.kt gehostet mit ❀ von GitHub

Fassadenvorbereitung:

Klasse KittenComponent {
Spaß onViewCreated ( Ansicht : KittenView ) {
}}
Spaß onStart () {
}}
Spaß onStop () {
}}
Spaß onViewDestroyed () {
}}
Spaß onDestroy () {
}}
}}

Viele Android-Entwickler kennen den Lebenszyklus. Es ist großartig fĂŒr iOS und sogar JavaScript. Das Diagramm des Übergangs zwischen den ZustĂ€nden des Lebenszyklus unserer Fassade sieht folgendermaßen aus: Ich werde


kurz erklÀren, was hier passiert:

  • Zuerst wird die onCreate-Methode aufgerufen, danach - onViewCreated und dann - onStart: Dadurch wird die Fassade in einen funktionsfĂ€higen Zustand versetzt (gestartet).
  • Irgendwann danach wird die onStop-Methode aufgerufen: Dadurch wird die Fassade gestoppt (gestoppt).
  • In einem gestoppten Zustand kann eine von zwei Methoden aufgerufen werden: onStart oder onViewDestroyed, dh entweder kann die Fassade erneut gestartet oder ihre Ansicht zerstört werden.
  • Wenn die Ansicht zerstört wird, kann sie entweder erneut erstellt werden (onViewCreated) oder die gesamte Fassade kann zerstört werden (onDestroy).

Die Fassadenimplementierung könnte folgendermaßen aussehen:

Klasse KittenComponent {
privater val store =
KittenStoreImpl (
network = KittenStoreNetwork (dataSource = KittenDataSource ()),
Parser = KittenStoreParser
)
private var view : KittenView? = null
private var startStopScope : DisposableScope? = null
Spaß onViewCreated ( Ansicht : KittenView ) {
diese .view = view
}}
fun onStart() {
val view = requireNotNull(view)
startStopScope = disposableScope {
store.subscribeScoped(onNext = view::render)
view.events.map(Event::toIntent).subscribeScoped(onNext = store::onNext)
}
}
fun onStop() {
startStopScope?.dispose()
}
fun onViewDestroyed() {
view = null
}
fun onDestroy() {
store.dispose()
}
}

Wie es funktioniert:

  • Zuerst erstellen wir eine Instanz von KittenStore.
  • In der onViewCreated-Methode erinnern wir uns an den Link zu KittenView.
  • In onStart signieren wir KittenStore und KittenView miteinander.
  • in onStop spiegeln wir sie voneinander;
  • In onViewDestroyed löschen wir den Link zur Ansicht.
  • Zerstöre in onDestroy KittenStore.

Fazit


Dies war der erste Artikel in meiner MVI-Serie bei Kotlin Multiplatform. Darin wir:

  • erinnerte sich, was MVI ist und wie es funktioniert;
  • die einfachste MVI-Implementierung auf Kotlin Multiplatform mithilfe der Reaktive Bibliothek durchgefĂŒhrt;
  • hat ein gemeinsames Modul zum Laden einer Liste von Bildern mit MVI erstellt.

Beachten Sie die wichtigsten Eigenschaften unseres gemeinsamen Moduls:

  • Es ist uns gelungen, den gesamten Code mit Ausnahme des UI-Codes in das Multiplattform-Modul einzufĂŒgen. Alle Logik sowie die Verbindungen und Konvertierungen zwischen Logik und BenutzeroberflĂ€che sind gemeinsam.
  • Logik und BenutzeroberflĂ€che sind völlig unabhĂ€ngig voneinander.
  • Die Implementierung der BenutzeroberflĂ€che ist sehr einfach: Sie mĂŒssen nur die eingehenden Ansichtsmodelle anzeigen und Ereignisse auslösen.
  • Die Modulintegration ist ebenfalls einfach: Sie benötigen lediglich:
    • KittenView-Schnittstelle (Protokoll) implementieren;
    • Erstellen Sie eine Instanz von KittenComponent.
    • seine Lebenszyklusmethoden zur richtigen Zeit nennen;
  • Dieser Ansatz vermeidet das „Durchsickern“ von Rx (oder Coroutine) in die Plattformen, was bedeutet, dass wir keine Abonnements auf Anwendungsebene verwalten mĂŒssen.
  • Alle wichtigen Klassen werden durch Schnittstellen abstrahiert und getestet.

Im nÀchsten Teil werde ich in der Praxis zeigen, wie die KittenComponent-Integration in iOS- und Android-Anwendungen aussieht.

Folgen Sie mir auf Twitter und bleiben Sie in Verbindung!

All Articles