Warum Sie MVP aus Ihren Projekten werfen sollten

Hallo allerseits! Heute möchte ich über die Architektur von Android-Anwendungen sprechen.
Tatsächlich mag ich Berichte und Artikel zu diesem Thema nicht wirklich, aber kürzlich bin ich zu der Erkenntnis gekommen, mit der ich teilen möchte.


Als ich anfing, mich mit Architekturen vertraut zu machen, fiel mein Blick auf MVP. Ich mochte die Einfachheit und die Verfügbarkeit einer großen Menge an Schulungsmaterialien.
Aber im Laufe der Zeit bemerkte ich, dass etwas nicht stimmte. Es gab das Gefühl, dass es besser möglich ist.


Fast alle Implementierungen, die ich gesehen habe, sahen so aus: Wir haben eine abstrakte Klasse, von der wir alle unsere Moderatoren erben.


class MoviePresenter(private val repository: Repository) : BasePresenter<MovieView>() { fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> view?.showError() is Either.Right -> view?.showMovies(result.value) } } } } 

Wir erstellen auch eine Ansichtsoberfläche für jeden Bildschirm, mit der der Präsentator arbeiten wird


 interface MovieView : MvpView { fun showMovies(movies: List<Movie>) fun showError() } 

Schauen wir uns die Nachteile dieses Ansatzes an:


  1. Sie müssen für jeden Bildschirm eine Ansichtsoberfläche erstellen. Bei großen Projekten werden wir viel zusätzlichen Code und Dateien haben, die die Paketnavigation erschweren.
  2. Presenter ist schwer wiederzuverwenden, da es an die Ansicht gebunden ist und bestimmte Methoden haben kann.
  3. Eine bestimmte Bedingung fehlt. Stellen Sie sich vor, wir stellen eine Anfrage an das Netzwerk und in diesem Moment stirbt unsere Aktivität und es wird eine neue erstellt. Daten kamen, wenn View noch nicht an Presenter gebunden ist. Dies wirft die Frage auf, wie diese Daten angezeigt werden sollen, wenn die Ansicht an Presenter gebunden ist. Antwort: nur Krücken. Moxy verfügt beispielsweise über einen ViewState, in dem die ViewCommand-Liste gespeichert ist. Diese Lösung funktioniert, aber es scheint mir, dass das Ziehen des Codes zum Speichern des Ansichtsstatus überflüssig ist (Multidex ist viel näher als Sie denken. Außerdem beginnt die Assembly mit der Verarbeitung von Anmerkungen, wodurch sie länger wird. Ja, Sie werden sagen, dass wir jetzt haben inkrementelles kapt, aber bestimmte Bedingungen sind für seinen Betrieb notwendig). Plus ViewCommand sind nicht paketierbar oder serialisierbar, was bedeutet, dass wir sie im Todesfall des Prozesses nicht speichern können. Es ist wichtig, einen dauerhaften Zustand zu haben, um nichts zu verlieren. Das Fehlen eines bestimmten Zustands ermöglicht es auch nicht, ihn zentral zu ändern, und dies kann zu schwierig zu reproduzierenden Fehlern führen.

Mal sehen, ob diese Probleme in anderen Architekturen gelöst sind.


MVVM


 class MovieViewModel(private val repository: Repository) { val moviesObservable: ObservableProperty<List<Movie>> = MutableObservableProperty() val errorObservable: ObservableProperty<Throwable> = MutableObservableProperty() fun loadMovies() { coroutineScope.launch { when (val result = repository.loadMovies()) { is Either.Left -> errorObservable.value = result.value is Either.Right -> moviesObservable.value = result.value } } } } 

Lassen Sie uns die oben genannten Punkte durchgehen:


  1. In MVVM verfügt VIew nicht mehr über eine Schnittstelle, da es einfach beobachtbare Felder im ViewModel abonniert.
  2. ViewModel ist einfacher wiederzuverwenden, da es nichts über View weiß. (folgt aus dem ersten Absatz)
  3. In MVVM ist das Statusproblem gelöst, aber nicht vollständig. In diesem Beispiel haben wir eine Eigenschaft im ViewModel, von der View die Daten bezieht. Wenn wir eine Anfrage an das Netzwerk senden, werden die Daten in der Unterkunft gespeichert und View erhält beim Abonnieren gültige Daten (und Sie müssen nicht einmal mit einem Tamburin tanzen). Wir können die Eigenschaft auch dauerhaft machen, so dass sie im Falle des Todes des Prozesses erhalten bleibt.

MVI


Definieren Sie Aktionen, Nebenwirkungen und Status


 sealed class Action { class LoadAction(val page: Int) : Action() class ShowResult(val result: List<Movie>) : Action() class ShowError(val error: Throwable) : Action() } sealed class SideEffect { class LoadMovies(val page: Int) : SideEffect() } data class State( val loading: Boolean = false, val data: List<Movie>? = null, val error: Throwable? = null ) 

Als nächstes kommt Reducer


 val reducer = { state: State, action: Action -> when (action) { is Action.LoadAction -> state.copy(loading = true, data = null, error = null) to setOf( SideEffect.LoadMovies(action.page) ) is Action.ShowResult -> state.copy( loading = false, data = action.result, error = null ) to emptySet() is Action.ShowError -> state.copy( loading = false, data = null, error = action.error ) to emptySet() } } 

und EffectHandler für die Behandlung von SideEffects


 class MovieEffectHandler(private val movieRepository: MovieRepository) : EffectHandler<SideEffect, Action> { override fun handle(sideEffect: SideEffect) = when (sideEffect) { is SideEffect.LoadMovies -> flow { when (val result = movieRepository.loadMovies(sideEffect.page)) { is Either.Left -> emit(Action.ShowError(result.value)) is Either.Right -> emit(Action.ShowResult(result.value)) } } } } 

Was haben wir:


  1. In MVI müssen wir auch keine Reihe von Verträgen für View erstellen. Sie müssen nur die Renderfunktion (Status) definieren.
  2. Dies wiederzuverwenden ist leider nicht so einfach, da wir einen Staat haben, der sehr spezifisch sein kann.
  3. In MVI haben wir einen bestimmten Zustand, den wir durch die Reduktionsfunktion zentral ändern können. Dank dessen können wir Statusänderungen verfolgen. Schreiben Sie beispielsweise alle Änderungen in das Protokoll. Dann können wir den letzten Status lesen, wenn die Anwendung abstürzt. Der Plus-Status kann dauerhaft sein, sodass Sie den Tod des Prozesses bewältigen können.

Zusammenfassung


MVVM löst das Problem des Prozesstodes. Leider ist der Zustand hier noch ungewiss und kann sich nicht zentral ändern. Dies ist natürlich ein Minus, aber die Situation wurde immer noch deutlich besser als bei MVP. MVI löst das Statusproblem, aber der Ansatz selbst kann etwas kompliziert sein. Außerdem gibt es ein Problem mit der Benutzeroberfläche, da das aktuelle UI-Toolkit in Android fehlerhaft ist. In MVVM aktualisieren wir die Benutzeroberfläche in Teilen, und in MVI bemühen wir uns, sie als Ganzes zu aktualisieren. Daher verhält sich MVVM für eine zwingende Benutzeroberfläche besser. Wenn Sie MVI verwenden möchten, empfehle ich Ihnen, sich mit der Theorie des virtuellen / inkrementellen DOM und den Bibliotheken für Android vertraut zu machen: Litho, Amboss, Jetpack Compose (Sie müssen warten). Oder Sie können Unterschiede mit Ihren Händen nehmen.


Aufgrund der oben genannten Daten würde ich Ihnen raten, beim Entwerfen einer Anwendung zwischen MVVM und MVI zu wählen. So erhalten Sie einen moderneren und bequemeren Ansatz (insbesondere in der Realität von Android).


Bibliotheken, die bei der Umsetzung dieser Ansätze helfen können:
MVVM - https://github.com/Miha-x64/Lychee
MVI - https://github.com/egroden/mvico , https://github.com/badoo/MVICore , https://github.com/arkivanov/MVIDroid


Vielen Dank für Ihre Aufmerksamkeit!

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


All Articles