Zweck dieses Artikels
Ich werde nicht näher darauf eingehen, wie MVI technisch implementiert ist (es gibt mehr als einen Weg und jeder hat seine eigenen Vor- und Nachteile). Mein Hauptziel in einem kurzen Artikel ist es, Sie zu interessieren, dieses Thema in Zukunft zu studieren und Sie möglicherweise zu ermutigen, dieses Muster in Ihren Kampfprojekten umzusetzen oder es zumindest in hausgemachten Vorbereitungen zu überprüfen.
Welchem Problem können Sie begegnen?
Mein lieber Freund, stellen wir uns diese Situation vor, wir haben eine Ansichtsoberfläche, mit der wir
arbeiten können:
interface ComplexView {
fun showLoading()
fun hideLoading()
fun showBanner()
fun hideBanner()
fun dataLoaded(names: List<String>)
fun showTakeCreditDialog()
fun hideTakeCreditDialog()
}
Auf den ersten Blick scheint nichts kompliziert zu sein. Sie wählen einfach eine separate Entität aus, um mit dieser Ansicht zu arbeiten, nennen sie einen Moderator (voila, der MVP ist bereit), und dies sind
Und hier ist der Moderator selbst:
interface Presenter {
fun onLoadData(dataKey: String)
fun onLoadCredit()
}
Es ist ganz einfach: In der Ansicht werden die Methoden des Präsentators abgerufen, wenn Daten geladen werden müssen. Der Präsentator hat seinerseits das Recht, die Ansicht abzurufen, um die geladenen Informationen sowie den Fortschritt anzuzeigen. Aber hier tritt das
Zum Beispiel möchten wir einen Dialog anzeigen , der dem Benutzer
view.hideTakeCreditDialog ()
Gleichzeitig sollten Sie jedoch nicht vergessen, dass Sie beim Anzeigen eines Dialogfelds das Laden ausblenden und nicht anzeigen müssen, während ein Dialogfeld auf dem Bildschirm angezeigt wird. Außerdem gibt es eine Methode, mit der ein Banner angezeigt wird, die wir nicht aufrufen sollten, während ein Dialogfeld angezeigt wird (oder das Dialogfeld schließen und erst danach das Banner anzeigen, hängt alles von den Anforderungen ab). Sie haben das folgende Bild.
In keinem Fall sollten Sie anrufen:
view.showBanner ()
view.showLoading ()
Während der Dialog angezeigt wird. Andernfalls werden
Und jetzt denken wir mit Ihnen und nehmen an, dass ich trotzdem ein Banner zeigen wollte (eine solche Anforderung von einem Unternehmen). Woran müssen Sie sich erinnern?
Tatsache ist, dass beim Aufrufen dieser Methode:
view.showBanner ()
Rufen Sie unbedingt an:
view.hideLoading ()
view.hideTakeCreditDialog ()
Wiederum, damit nichts über die anderen Elemente auf dem Bildschirm sprang, die berüchtigte Konsistenz.
Es stellt sich also die Frage, wer Sie auf die Hände schlägt, wenn Sie etwas falsch machen. Die Antwort ist einfach - NIEMAND . In einer solchen Erkenntnis haben Sie absolut keine Kontrolle.
Vielleicht müssen Sie in Zukunft der Ansicht weitere Funktionen hinzufügen, die sich auch auf das beziehen, was bereits vorhanden ist. Was sind die Nachteile, die wir daraus ziehen?
- Nudeln aus staatlichen Abhängigkeiten von Yuan-Elementen
- Die Logik der Übergänge von einem Anzeigezustand in einen anderen wird über den
Präsentator verschmiert - Es ist ziemlich schwierig, einen neuen Bildschirmstatus hinzuzufügen, da ein hohes Risiko besteht, dass
Sie vergessen, etwas auszublenden, bevor Sie ein neues Banner oder einen neuen Dialog anzeigen
Und Sie und ich haben den Fall analysiert, wenn nur 7 Methoden in der Ansicht sind. Und auch hier stellte sich heraus, dass es ein Problem war.
Aber es gibt solche Ansichten:
interface ChatView : IView<ChatPresenter> {
fun setMessage(message: String)
fun showFullScreenProgressBar()
fun updateExistingMessage(model: ChatMessageModel)
fun hideFullScreenProgressBar()
fun addNewMessage(localMessage: ChatMessageModel)
fun showErrorFromLoading(message: String)
fun moveChatToStart()
fun containsMessage(message: ChatMessageModel): Boolean
fun getChatMessagesSize(): Int fun getLastMessage(): ChatMessageModel?
fun updateMessageStatus(messageId: String, status: ChatMessageStatus)
fun setAutoLoading(autoLoadingEnabled: Boolean)
fun initImageInChat(needImageInChat: Boolean)
fun enableNavigationButton()
fun hideKeyboard()
fun scrollToFirstMessage()
fun setTitle(@StringRes titleRes: Int)
fun setVisibleSendingError(isVisible: Boolean)
fun removeMessage(localId: String)
fun setBottomPadding(hasPadding: Boolean)
fun initMessagesList(pageSize: Int)
fun showToast(@StringRes textRes: Int)
fun openMessageDialog(message: String)
fun showSuccessRating()
fun setRatingAvailability(isEnabled: Boolean)
fun showSuccessRatingWithResult(ratingValue: String)
}
Es wird ziemlich schwierig sein, hier etwas Neues hinzuzufügen oder das Alte zu bearbeiten. Man muss sehen, was und wie miteinander verbunden ist, und dann anfangen zu
MVI
Der springende Punkt
Die Quintessenz ist, dass wir eine Entität haben, die als Staat bezeichnet wird. Basierend auf diesem Status wird die Ansicht in der Ansicht gerendert. Ich werde nicht tief gehen, also ist es meine Aufgabe, Ihr Interesse zu wecken, also werde ich direkt zu Beispielen gehen. Und am Ende des Artikels finden Sie eine Liste sehr nützlicher Quellen, wenn Sie interessiert sind.
Erinnern wir uns an unsere Position am Anfang des Artikels. Wir haben eine Ansicht, in der wir Dialoge, Banner
data class UIState(
val loading: Boolean = false,
val names: List<String>? = null,
val isBannerShowing: Boolean = false,
val isCreditDialogShowing: Boolean = false
)
Lassen Sie uns die Regel festlegen, Sie und ich können die Ansicht nur mit Hilfe dieses Status ändern, es wird eine solche Schnittstelle geben:
interface ComplexView {
fun renderState(state: UIState)
}
Jetzt setzen wir noch eine Regel. Wir können den Eigentümer des Staates (in unserem Fall den Moderator) nur über einen Einstiegspunkt kontaktieren. Durch das Senden von Ereignissen an ihn. Es ist eine gute Idee, diese Ereignisse als Aktionen zu bezeichnen.
sealed class UIAction {
class LoadNamesAction(dataKey: String) : UIAction()
object LoadBannerAction : UIAction()
object LoadCreditDialogInfo : UIAction()
}
Wirf nur keine Tomaten für versiegelte Klassen auf mich, sie vereinfachen das Leben in der aktuellen Situation und eliminieren zusätzliche Kasten bei der Verarbeitung von Aktionen im Moderator. Ein Beispiel finden Sie weiter unten. Die Presenter-Oberfläche sieht folgendermaßen aus:
interface Presenter {
fun processAction(action: UIAction)
}
Lassen Sie uns nun darüber nachdenken, wie Sie das Ganze verbinden können:
fun processAction(action: UiAction): UIState {
return when (action) {
is UiAction.LoadNamesAction -> state.copy(
loading = true,
isBannerShowing = false,
isCreditDialogShowing = false
)
is UiAction.LoadBannerAction -> state.copy(
loading = false,
isBannerShowing = true,
isCreditDialogShowing = false
)
is UiAction.LoadCreditDialogInfo -> state.copy(
loading = false,
isBannerShowing = false,
isCreditDialogShowing = true
)
}
}
Wenn Sie darauf geachtet haben, findet der Fluss von einem Anzeigezustand zum anderen jetzt an einem Ort statt und es ist bereits einfacher, ein Bild davon zusammenzustellen, wie alles in Ihrem Kopf funktioniert.
Es ist nicht super einfach, aber dein Leben sollte einfacher sein. In meinem Beispiel ist dies nicht sichtbar, aber wir können entscheiden, wie unser neuer Status basierend auf dem vorherigen Status verarbeitet werden soll (es gibt auch mehrere Begriffe, um dies zu implementieren). Ganz zu schweigen von der wahnsinnigen Wiederverwendbarkeit, die die Jungs von badoo erreicht haben. Einer ihrer Assistenten bei der Erreichung dieses Ziels war MVI.
Sie sollten sich jedoch nicht früh freuen, alles auf dieser Welt hat Vor- und Nachteile, und hier sind sie
- Die übliche Toastshow bricht uns
- Wenn Sie ein Kontrollkästchen aktualisieren, wird der gesamte Status erneut kopiert und an die
Ansicht gesendet. Das heißt, es wird unnötig neu gezeichnet, wenn nichts dagegen unternommen wird
Angenommen, wir möchten einen normalen Android-Toast anzeigen. Gemäß der aktuellen Logik setzen wir in unserem Status ein Flag, um unseren Toast anzuzeigen.
data class UIState(
val showToast: Boolean = false,
)
Der Erste
Wir nehmen und ändern den Status im Presenter, setzen showToast = true und das Einfachste, was passieren kann, ist die Bildschirmdrehung. Alles wird zerstört,
Nun, der zweite
Dies ist bereits ein Problem des unnötigen Renderns in der Ansicht, das jedes Mal auftritt, selbst wenn sich nur eines der Felder im Status ändert. Und dieses Problem wird auf mehrere, manchmal nicht die schönsten Arten gelöst (manchmal durch eine langweilige Prüfung, bevor man sich über die neue Bedeutung beschwert, dass sie sich von der vorherigen unterscheidet). Aber mit der Veröffentlichung von compose in einer stabilen Version wird dieses Problem gelöst, dann wird mein Freund mit Ihnen in einer verwandelten und glücklichen Welt leben!
Zeit für die Profis:
- Ein Einstiegspunkt in die Ansicht
- Wir haben immer den aktuellen Status des Bildschirms zur Hand
- Bereits in der Implementierungsphase müssen Sie darüber nachdenken, wie ein Zustand
in einen anderen fließt und welche Verbindung zwischen ihnen besteht - Unidirektionaler Datenfluss
Liebe Android und verliere nie deine Motivation!
Liste meiner Inspiratoren
- www.youtube.com/watch?v=VsStyq4Lzxo&t=592s - Deklarative UI-Muster (Google
I / O'19) - www.youtube.com/watch?v=pXw6r2kAvq8&t=2s - Architekturreise von Zsolt
Kocsi, Badoo EN
- www.youtube.com/watch?v=hBkQkjWnAjg&t=318s - Wie man gut
getrocknetes MVI für Android kocht - www.youtube.com/watch?v=0IKHxjkgop4 - Managing State mit RxJava von Jake
Wharton - hannesdorfmann.com/android/model-view-intent - Artikel von Hannes Doorfmann