Asynchrone Task Execution Layer-Architektur

In mobilen Anwendungen von sozialen Netzwerken mag der Benutzer, schreibt einen Kommentar, blättert dann durch den Feed, startet das Video und setzt das Gleiche erneut. All dies ist schnell und fast gleichzeitig. Wenn die Implementierung der Geschäftslogik der Anwendung vollständig blockiert ist, kann der Benutzer erst dann zum Band gehen, wenn dergleichen zum Aufzeichnen mit Siegeln geladen ist. Der Benutzer wird jedoch nicht warten, daher funktionieren in den meisten mobilen Anwendungen asynchrone Aufgaben, die unabhängig voneinander gestartet und ausgeführt werden. Der Benutzer führt mehrere Aufgaben gleichzeitig aus und blockiert sich nicht gegenseitig. Eine asynchrone Aufgabe wird gestartet und ausgeführt, während der Benutzer die nächste startet.



Bei der Entschlüsselung des Berichts von Stepan Goncharov auf AppsConf werden wir auf die Asynchronität eingehen : Wir werden uns mit der Architektur mobiler Anwendungen befassen, diskutieren, warum wir eine separate Schicht für die Ausführung asynchroner Aufgaben trennen müssen, wir werden die Anforderungen und vorhandenen Lösungen analysieren, wir werden die Vor- und Nachteile analysieren und eine der Implementierungen dieses Ansatzes betrachten. Wir lernen auch, wie asynchrone Aufgaben verwaltet werden, warum jede Aufgabe ihre eigene ID hat, welche Ausführungsstrategien gelten und wie sie die Entwicklung der gesamten Anwendung vereinfachen und beschleunigen.


Über den Sprecher: Stepan Goncharov ( stepango ) arbeitet bei Grab - es ist wie bei Uber, aber in Südostasien. Er ist seit mehr als 9 Jahren in der Android-Entwicklung tätig. Interessiert an Kotlin seit 2014 und seit 2016 - verwendet es im Produkt. Organisiert von der Kotlin User Group in Singapur. Dies ist einer der Gründe, warum alle Codebeispiele auf Kotlin sein werden und nicht, weil es in Mode ist.

Wir werden uns einen Ansatz zum Entwerfen der Komponenten Ihrer Anwendung ansehen. Dies ist eine Handlungsanleitung für diejenigen, die der Anwendung neue Komponenten hinzufügen, diese bequem entwerfen und dann erweitern möchten. iOS-Entwickler können den iOS-Ansatz verwenden. Der Ansatz gilt auch für andere Plattformen. Ich interessiere mich seit 2014 für Kotlin, daher werden alle Beispiele in dieser Sprache sein. Aber keine Sorge - Sie können dasselbe in Swift, Objective-C und anderen Sprachen schreiben.

Beginnen wir mit den Problemen und Nachteilen von Reactive Extensions . Probleme sind typisch für andere asynchrone Grundelemente, daher sagen wir RX - denken Sie an die Zukunft und das Versprechen, und alles wird ähnlich funktionieren.

RX-Probleme


Hohe Eintrittsschwelle . RX ist recht komplex und groß - es hat 270 Bediener und es ist nicht einfach, dem gesamten Team den richtigen Umgang damit beizubringen. Wir werden dieses Problem nicht diskutieren - es geht über den Rahmen des Berichts hinaus.

In RX müssen Sie Ihre Abonnements manuell verwalten und den Lebenszyklus der Anwendung überwachen . Wenn Sie Single oder Observable bereits abonniert haben, können Sie es nicht mit einem anderen SIngle vergleichen , da Sie immer ein neues Objekt erhalten und es zur Laufzeit immer andere Abonnements gibt. In RX gibt es keine Möglichkeit, Abonnements und Streams zu vergleichen .

Wir werden versuchen, einige dieser Probleme zu lösen. Wir werden jedes Problem einmal lösen und dann das Ergebnis wiederverwenden.

Problem Nummer 1: Eine Aufgabe mehrmals ausführen


Ein häufiges Problem bei der Entwicklung ist unnötiges Arbeiten und mehrmaliges Wiederholen derselben Aufgaben. Stellen Sie sich vor, wir haben ein Formular zur Dateneingabe und eine Schaltfläche zum Speichern. Wenn diese Taste gedrückt wird, wird eine Anfrage gesendet. Wenn Sie jedoch während des Speicherns des Formulars mehrmals auf klicken, werden mehrere identische Anfragen gesendet. Wir haben den Knopf zum Testen der Qualitätssicherung gegeben, sie haben 40 Mal in einer Sekunde gedrückt - wir haben 40 Anfragen erhalten, weil zum Beispiel die Animation keine Zeit zum Arbeiten hatte.

Wie löse ich das Problem? Jeder Entwickler hat seinen eigenen bevorzugten Lösungsansatz: Einer hält eine debounce , der andere blockiert die Schaltfläche für alle Fälle durch clickable = false . Es gibt keinen allgemeinen Ansatz, daher werden diese Fehler in unserer Anwendung entweder angezeigt oder verschwinden. Wir lösen das Problem nur, wenn die Qualitätssicherung uns sagt: "Oh, ich habe hier geklickt und es ist kaputt gegangen"!

Eine skalierbare Lösung?


Um solche Situationen zu vermeiden, werden wir RX oder ein anderes asynchrones Framework umbrechen - wir werden allen asynchronen Operationen IDs hinzufügen . Die Idee ist einfach - wir brauchen eine Möglichkeit, sie zu vergleichen, da diese Methode normalerweise nicht in den Frameworks enthalten ist. Wir können die Aufgabe abschließen, wissen jedoch nicht, ob sie bereits abgeschlossen wurde oder nicht.

Nennen wir unseren Wrapper "Act" - andere Namen sind bereits vergeben. Erstellen Sie dazu kleine typealias und eine einfache interface in der es nur ein Feld gibt:

 typealias Id = String interface Act { val id: Id } 

Dies ist praktisch und reduziert die Codemenge geringfügig. Wenn es String später nicht gefällt, werden wir es durch etwas anderes ersetzen. In diesem kleinen Code beobachten wir eine lustige Tatsache.

Schnittstellen können Eigenschaften enthalten.

Für Programmierer, die aus Java kommen, ist dies unerwartet. Normalerweise fügen sie getId() -Methoden in die Schnittstelle ein, aber dies ist aus Sicht von Kotlin die falsche Lösung.

Wie werden wir gestalten?


Ein kleiner Exkurs. Beim Entwerfen halte ich mich an zwei Prinzipien. Die erste besteht darin , die Komponentenanforderungen und die Implementierung in kleine Teile zu zerlegen . Dies ermöglicht eine detaillierte Kontrolle über das Schreiben von Code. Wenn Sie eine große Komponente erstellen und versuchen, alles auf einmal zu erledigen, ist dies schlecht. Normalerweise funktioniert diese Komponente nicht und Sie beginnen mit dem Einsetzen von Krücken. Ich fordere Sie daher dringend auf, in kleinen kontrollierten Schritten zu schreiben und sie zu genießen. Das zweite Prinzip besteht darin, die Funktionsfähigkeit nach jedem Schritt zu überprüfen und den Vorgang erneut zu wiederholen .

Warum reicht der Ausweis nicht aus?


Kommen wir zurück zum Problem. Wir haben den ersten Schritt gemacht - wir haben eine ID hinzugefügt und alles war einfach - die Schnittstelle und das Feld. Dies hat uns nichts gebracht, da die Schnittstelle keine Implementierung enthält und nicht alleine funktioniert, aber Sie können Vorgänge vergleichen.

Als nächstes werden wir Komponenten hinzufügen, die es uns ermöglichen, die Schnittstelle zu verwenden und zu verstehen, dass wir eine Art Anfrage ein zweites Mal ausführen möchten, wenn dies nicht erforderlich ist. Als erstes werden wir neue Abstraktionen einführen .

Einführung neuer Abstraktionen: MapDisposable


Es ist wichtig, den richtigen Namen und die richtige Abstraktion zu wählen, die Entwicklern bekannt sind, die in Ihrer Codebasis arbeiten. Da ich Beispiele für RX habe, werden wir das RX-Konzept und ähnliche Namen verwenden wie die Bibliotheksentwickler. So können wir unseren Kollegen leicht erklären, was sie getan haben, warum und wie es funktionieren sollte. Informationen zum Auswählen eines Namens finden Sie in der CompositeDiposable-Dokumentation .

Erstellen wir eine kleine MapDisposable-Oberfläche, die Informationen zu aktuellen Aufgaben enthält und beim Löschen dispose () aufruft . Ich werde die Implementierung nicht geben, Sie können alle Quellen auf meinem GitHub sehen .

Wir nennen MapDisposable auf diese Weise, da die Komponente wie eine Map funktioniert, jedoch CompositeDiposable-Eigenschaften aufweist.

Einführung neuer Abstraktionen: ActExecutor


Die nächste abstrakte Komponente ist ActExecutor. Es startet oder startet keine neuen Aufgaben, hängt von MapDisposable ab und delegiert die Fehlerbehandlung. So wählen Sie einen Namen aus - siehe Dokumentation .

Nehmen Sie die nächste Analogie aus dem JDK. Es hat einen Executor, in dem Sie Thread übergeben und etwas tun können. Es scheint mir, dass dies eine coole Komponente ist und gut gestaltet, also nehmen wir es als Grundlage.

Wir erstellen ActExecutor und eine einfache Schnittstelle dafür, wobei wir dem Prinzip einfacher kleiner Schritte folgen. Der Name selbst sagt, dass es eine Komponente ist, an die wir etwas übertragen, und es beginnt, etwas zu tun. ActExecutor hat eine Methode, mit der wir Act und für alle Fälle Fehler behandeln, denn ohne sie gibt es keinen Weg.

 interface ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit = ::logError) } interface MapDisposable { fun contains(id: Id): Boolean fun add(id: Id, disposable: () -> T) fun remove(id: Id) } 

MapDisposable ist ebenfalls eingeschränkt: Nehmen Sie die Map-Oberfläche und kopieren Sie die contains Methoden, add und remove sie. Die add Methode unterscheidet sich von Map: Das zweite Argument ist das Lambda für Schönheit und Bequemlichkeit. Der Vorteil ist, dass wir das Lambda synchronisieren können, um unerwartete Rennbedingungen zu vermeiden. Aber wir werden nicht darüber reden, wir werden weiter über Architektur sprechen.

Schnittstellenimplementierung


Wir haben alle Schnittstellen deklariert und werden versuchen, etwas Einfaches zu implementieren. Nehmen Sie CompletableAct und SingleAct .

 class CompletableAct ( override val id: Id, override val completable: Completable ) : Act class SingleAct<T : Any>( override val id: Id, override val single: Single<T> ) : Act 

CompletableAct ist ein Wrapper über Completable. In unserem Fall enthält es einfach eine ID - was wir brauchen. SingleAct ist fast das gleiche. Wir können Maybe und Flowable ebenfalls implementieren, bleiben aber bei den ersten beiden Implementierungen.

Für Single haben wir den generischen Typ <T : Any> . Als Kotlin-Entwickler bevorzuge ich einen solchen Ansatz.

Versuchen Sie, Nicht-Null-Generika zu verwenden.

Nachdem wir nun eine Reihe von Schnittstellen haben, implementieren wir eine Logik, um die Ausführung derselben Anforderungen zu verhindern.

 class ActExecutorImpl ( val map: MapDisposable ): ActExecutor { fun execute( act: Act, e: (Throwable) -> Unit ) = when { map.contains(act.id) -> { log("${act.id} - in progress") } else startExecution(act, e) log("${act.id} - Started") } } 

Wir nehmen eine Karte und prüfen, ob eine Anfrage darin enthalten ist. Wenn nicht, beginnen wir mit der Ausführung der Anforderung und fügen sie zur Laufzeit zur Map hinzu. Löschen Sie nach der Ausführung mit einem Ergebnis: Fehler oder Erfolg die Anforderung aus Map.

Für sehr aufmerksame - es gibt keine Synchronisation, aber die Synchronisation ist im Quellcode auf GitHub.

 fun startExecution(act: Act, e: (Throwable) -> Unit) { val removeFromMap = { mapDisposable.remove(act.id) } mapDisposable.add(act.id) { when (act) { is CompletableAct -> act.completable .doFinally(removeFromMap) .subscribe({}, e) is SingleAct<*> -> act.single .doFinally(removeFromMap) .subscribe({}, e) else -> throw IllegalArgumentException() } } 

Verwenden Sie Lambdas als letztes Argument, um die Lesbarkeit des Codes zu verbessern. Es ist wunderschön und Ihre Kollegen werden es Ihnen danken.

Wir werden weitere Kotlin-Chips verwenden und Erweiterungsfunktionen für Completable und Single hinzufügen. Mit ihnen müssen wir nicht nach einer Factory-Methode suchen, um einen CompletableAct und einen SingleAct zu erstellen - wir werden sie über Erweiterungsfunktionen erstellen.

 fun Completable.toAct(id: Id): Act = CompletableAct(id, this) fun <T: Any> Single<T>.toAct(id: Id): Act = SingleAct(id, this) 

Erweiterungsfunktionen können zu jeder Klasse hinzugefügt werden.

Ergebnis


Wir haben mehrere Komponenten und eine sehr einfache Logik implementiert. Nun ist die Hauptregel, die wir befolgen müssen, kein Abonnement von Hand zu erzwingen . Wenn wir etwas ausführen möchten, geben wir es über Executor. Ebenso wie mit Thread - niemand startet sie selbst.

 fun act() = Completable.timer(2, SECONDS).toAct("Hello") executor.apply { execute(act()) execute(act()) execute(act()) } Hello - Act Started Hello - Act Duplicate Hello - Act Duplicate Hello - Act Finished 

Wir haben uns einmal im Team geeinigt, und jetzt gibt es immer eine Garantie dafür, dass die Ressourcen unserer Anwendung nicht für die Ausführung identischer und unnötiger Anforderungen verwendet werden.

Das erste Problem wurde gelöst. Erweitern wir nun die Lösung, um sie flexibel zu gestalten.

Problem Nummer 2: Welche Aufgabe muss abgebrochen werden?


Sowie in Fällen, in denen eine nachfolgende Anfrage storniert werden muss , müssen wir möglicherweise die vorherige stornieren. Zum Beispiel haben wir die Informationen über unseren Benutzer zum ersten Mal bearbeitet und an den Server gesendet. Aus irgendeinem Grund dauerte der Versand lange und wurde nicht abgeschlossen. Wir haben das Benutzerprofil erneut bearbeitet und dieselbe Anfrage ein zweites Mal gesendet. In diesem Fall ist es nicht sinnvoll, eine spezielle ID für die Anforderung zu generieren. Die Informationen aus dem zweiten Versuch sind relevanter und die vorherige Anforderung wird abgebrochen .

Die aktuelle Lösung funktioniert nicht, da die Ausführung der Anforderung immer mit relevanten Informationen abgebrochen wird. Wir müssen die Lösung irgendwie erweitern, um das Problem zu umgehen und mehr Flexibilität zu schaffen. Verstehen Sie dazu, was wir alle wollen? Aber wir wollen verstehen, welche Aufgabe abzubrechen ist, wie man sie nicht kopiert und einfügt und wie man sie nennt.

Komponenten hinzufügen


Wir nennen Strategien für das Abfrageverhalten und erstellen zwei Schnittstellen für diese: StrategyHolder und Strategy . Wir erstellen auch 2 Objekte, die für die anzuwendende Strategie verantwortlich sind.

 interface StrategyHolder { val strategy: Strategy } sealed class Strategy object KillMe : Strategy() object SaveMe : Strategy() 

Ich benutze keine Aufzählung - ich mag die versiegelte Klasse mehr . Sie sind leichter, verbrauchen weniger Speicher und lassen sich einfacher und bequemer erweitern.

Die versiegelte Klasse ist einfacher zu erweitern und kürzer zu schreiben.

Bestehende Komponenten aktualisieren


An diesem Punkt ist alles einfach. Wir hatten eine einfache Oberfläche, jetzt wird es der Erbe von StrategyHolder sein. Da es sich um Schnittstellen handelt, gibt es kein Problem mit der Vererbung. Bei der Implementierung von CompletableAct fügen wir eine weitere override und fügen dort den Standardwert hinzu, um sicherzustellen, dass die Änderungen mit dem vorhandenen Code kompatibel bleiben.

 interface Act : StrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe ) : Act 

Strategien


Ich habe mich für die SaveMe- Strategie entschieden, die mir offensichtlich erscheint. Diese Strategie storniert nur die folgenden Anforderungen - die erste Anforderung bleibt immer bestehen, bis sie abgeschlossen ist.

Wir haben ein wenig an unserer Implementierung gearbeitet. Wir hatten eine Ausführungsmethode und jetzt haben wir dort eine Strategieprüfung hinzugefügt.

  • Wenn die SaveMe- Strategie dieselbe ist wie zuvor, hat sich nichts geändert.
  • Wenn die Strategie KillMe ist, beenden Sie die vorherige Anforderung und starten Sie eine neue.

 override fun execute(act: Act, e: (Throwable) -> Unit) = when { map.contains(act.id) -> when (act.strategy) { KillMe -> { map.remove(act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) } 

Ergebnis


Wir konnten Strategien einfach verwalten, indem wir ein Minimum an Code geschrieben haben. Gleichzeitig sind unsere Kollegen glücklich und wir können so etwas tun.

 executor.apply { execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello", KillMe)) execute(Completable.timer(2, SECONDS) .toAct("Hello«, KillMe)) } Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Canceled Hello - Act Started Hello - Act Finished 

Wir erstellen eine asynchrone Aufgabe, übergeben die Strategie und jedes Mal, wenn wir eine neue Aufgabe starten, werden alle vorherigen und nicht die nächsten Aufgaben abgebrochen.

Problem Nummer 3: Strategien reichen nicht aus


Kommen wir zu einem interessanten Problem, auf das ich bei einigen Projekten gestoßen bin. Wir werden unsere Lösung erweitern, um kompliziertere Fälle zu behandeln. Einer dieser Fälle, der insbesondere für soziale Netzwerke relevant ist, ist „Gefällt mir / Gefällt mir nicht“ . Es gibt einen Beitrag, den wir mögen möchten, aber als Entwickler möchten wir nicht die gesamte Benutzeroberfläche blockieren und den Dialog im Vollbildmodus mit Laden anzeigen, bis die Anforderung abgeschlossen ist. Ja, und der Benutzer wird unglücklich sein. Wir wollen den Benutzer täuschen: Er drückt den Knopf und als ob das schon passiert wäre - hat eine schöne Animation begonnen. Aber tatsächlich gab es kein Vergleich - wir warten, bis die Täuschung wahr wird. Um Betrug zu verhindern, müssen wir Abneigungen gegen den Benutzer transparent behandeln.

Es wäre schön, dies richtig zu handhaben, damit der Benutzer das gewünschte Ergebnis erhält. Für uns als Entwickler ist es jedoch schwierig, jedes Mal unterschiedliche, sich gegenseitig ausschließende Anforderungen zu bearbeiten.

Es gibt zu viele Fragen. Wie kann man verstehen, dass Abfragen zusammenhängen? Wie speichere ich diese Verbindungen? Wie gehe ich mit komplexen Skripten um und nicht mit Kopieren und Einfügen? Wie benenne ich neue Komponenten? Die Aufgaben sind komplex und das, was wir bereits implementiert haben, ist für die Lösung nicht geeignet.

Gruppen und Strategien für Gruppen


Erstellen Sie eine einfache Schnittstelle mit dem Namen GroupStrategyHolder . Es ist etwas komplizierter - zwei Felder statt eines.

 interface GroupStrategyHolder { val groupStrategy: GroupStrategy val groupKey: String } sealed class GroupStrategy object Default : GroupStrategy() object KillGroup : GroupStrategy() 

Zusätzlich zur Strategie für eine bestimmte Anfrage führen wir eine neue Entität ein - eine Gruppe von Anfragen. Diese Gruppe wird auch Strategien haben. Wir werden nur die einfachste Option mit zwei Strategien betrachten: Standard - die Standardstrategie, wenn wir nichts mit Abfragen tun, und KillGroup - beendet alle vorhandenen Abfragen aus der Gruppe und startet eine neue.

 interface Act : StrategyHolder, GroupStrategyHolder { val id: String } class CompletableAct( override val id: String, override val completable: Completable, override val strategy: Strategy = SaveMe, override val groupStrategy: GroupStrategy = Default override val groupKey: String = "" ) : Act 

Wir wiederholen die Schritte, über die ich zuvor gesprochen habe: Wir nehmen die Schnittstelle, erweitern sie und fügen CompletableAct und SingleAct zwei zusätzliche Felder hinzu.

Implementierung aktualisieren


Wir kehren zur Execute-Methode zurück. Die dritte Aufgabe ist komplizierter, aber die Lösung ist recht einfach: Wir überprüfen die Gruppenstrategie auf eine bestimmte Anforderung und, wenn es sich um KillGroup handelt, beenden wir die gesamte Gruppe und führen die übliche Logik aus.

 MapDisposable -> GroupDisposable ... override fun execute(act: Act, e: (Throwable) -> Unit) { if (act.groupStrategy == KillGroup) groupDisposable.removeGroup(act.groupKey) return when { groupDisposable.contains(act.groupKey, act.id) -> when (act.strategy) { KillMe -> { stop(act.groupKey, act.id) startExecution(act, e) } SaveMe -> log("${act.id} - Act duplicate") } else -> startExecution(act, e) } } 

Das Problem ist komplex, aber wir haben bereits eine ziemlich angemessene Infrastruktur - wir können es erweitern und das Problem lösen. Was müssen wir jetzt tun, wenn Sie sich unser Ergebnis ansehen?

Ergebnis


 fun act(id: String)= Completable.timer(2, SECONDS).toAct( id = id, groupStrategy = KillGroup, groupKey = "Like-Dislike-PostId-1234" ) executor.apply { execute(act(“Like”)) execute(act(“Dislike”)) execute(act(“Like”)) } Like - Act Started Like - Act Canceled Dislike - Act Started Dislike - Act Canceled Like - Act Started Like - Act Finished 

Wenn wir solch komplexe Abfragen benötigen, fügen wir zwei Felder hinzu: groupStrategy und group ID. Die Gruppen-ID ist ein spezifischer Parameter, da Sie zur Unterstützung vieler paralleler Like / Dislike-Anforderungen eine Gruppe für jedes Anforderungspaar erstellen müssen, das zum selben Objekt gehört. In diesem Fall können Sie die Gruppe Like-Dislike-PostId benennen und dort die Post-ID hinzufügen. Jedes Mal, wenn wir die benachbarten Beiträge mögen, werden wir sicher sein, dass alles für den vorherigen und den nächsten Beitrag korrekt funktioniert.

In unserem synthetischen Beispiel versuchen wir, eine Like-Dislike-Like-Sequenz auszuführen. Wenn wir die erste und dann die zweite Aktion ausführen, wird die vorherige abgebrochen und die nächste wie die vorherige Abneigung. Das wollte ich.

Im letzten Beispiel haben wir benannte Parameter verwendet, um Acts zu erstellen. Dies trägt zur besseren Lesbarkeit des Codes bei, insbesondere wenn viele Parameter vorhanden sind.

Verwenden Sie zum leichteren Lesen benannte Parameter.

Architektur


Mal sehen, wie sich diese Entscheidung auf unsere Architektur auswirken kann. Bei Projekten sehe ich oft, dass das Ansichtsmodell oder der Präsentator viel Verantwortung übernehmen, wie z. B. Hacks, um die Situation irgendwie mit "Gefällt mir" / "Gefällt mir nicht" zu behandeln. Normalerweise all diese Logik im Ansichtsmodell: viel doppelter Code mit Tastensperre, LifeCycle-Handler, Abonnements.



Alles, was unser Executor jetzt tut, war einmal in Presenter oder View Model. Wenn die Architektur ausgereift ist, könnten die Entwickler diese Logik auf eine Art Interaktor oder Anwendungsfall übertragen, aber die Logik wurde an mehreren Stellen dupliziert.

Nachdem wir Executor übernommen haben, wird das Ansichtsmodell einfacher und die gesamte Logik ist ihnen verborgen. Wenn Sie dies einmal zu Presenter und dem Interaktor gebracht haben, wissen Sie, dass der Interaktor und der Presenter einfacher werden. Im Allgemeinen war ich zufrieden.



Was noch hinzuzufügen?


Ein weiteres Plus der aktuellen Lösung ist, dass sie erweiterbar ist. Was möchten wir noch als Entwickler hinzufügen, die an einer mobilen Anwendung arbeiten und jeden Tag mit Fehlern und vielen gleichzeitigen Anfragen zu kämpfen haben?

Die Möglichkeiten


Die Implementierung des Lebenszyklus blieb hinter den Kulissen, aber als mobile Entwickler denken wir alle immer darüber nach und sorgen uns, dass nichts wegfließt. Ich möchte Anwendungsneustartanforderungen speichern und wiederherstellen .

Anrufketten. Durch das Umschließen von RX-Ketten wird es möglich, diese zu serialisieren, da RX standardmäßig nicht serialisiert.

Nur wenige Benutzer wissen, wie viele gleichzeitige Anforderungen zu einem bestimmten Zeitpunkt in ihren Anwendungen ausgeführt werden. Ich würde nicht sagen, dass dies ein großes Problem für kleine und mittlere Anwendungen ist. Für eine große Anwendung, die im Hintergrund viel Arbeit leistet, ist es jedoch hilfreich, die Ursachen für Abstürze und Benutzerbeschwerden zu verstehen. Ohne zusätzliche Infrastruktur haben Entwickler einfach keine Informationen, um den Grund zu verstehen: Vielleicht liegt der Grund in der Benutzeroberfläche oder in einer großen Anzahl ständiger Anfragen im Hintergrund. Wir können unsere Lösung erweitern und Metriken hinzufügen.

Lassen Sie uns die Möglichkeiten genauer betrachten.

Lebenszyklusverarbeitung


 class ActExecutorImpl( lifecycle: Lifecycle ) : ActExecutor { inir { lifecycle.doOnDestroy { cancelAll() } } ... 

Dies ist ein Beispiel für eine Lebenszyklusimplementierung. Im einfachsten Fall: Destroy Fragmente Destroy oder mit Activity abgebrochen werden, übergeben wir den Lifecycle-Handler an unseren Executor . Wenn das Ereignis onDestroy auftritt, löschen wir alle Anforderungen . Dies ist eine einfache Lösung, bei der kein ähnlicher Code in Ansichtsmodelle kopiert und eingefügt werden muss. LifeData macht ungefähr das Gleiche.

Speichern / Wiederherstellen


Da wir Wrapper haben, können wir separate Klassen für Acts erstellen, in denen Logik zum Erstellen asynchroner Aufgaben vorhanden ist. Außerdem können wir diesen Namen in der Datenbank speichern und beim Start der Anwendung mit der Factory-Methode oder ähnlichem aus der Datenbank wiederherstellen .

Gleichzeitig erhalten wir die Möglichkeit, offline zu arbeiten, und starten die Anforderungen neu, die mit Fehlern abgeschlossen wurden, wenn das Internet angezeigt wird. In Abwesenheit des Internets oder bei Anforderungsfehlern speichern wir diese in der Datenbank und stellen sie dann wieder her und führen sie erneut aus. Wenn Sie dies mit normalem RX ohne zusätzliche Wrapper tun können, schreiben Sie bitte in die Kommentare, es wäre interessant.

Ketten anrufen


Wir können auch unsere Taten binden . Eine weitere Erweiterungsoption ist das Ausführen von Abfrageketten . Sie haben beispielsweise eine Entität, die auf dem Server erstellt werden muss, und eine andere Entität, die von der ersten abhängt, muss genau zu dem Zeitpunkt erstellt werden, zu dem wir sicher sind, dass die erste Anforderung erfolgreich war. Dies kann auch durchgeführt werden. Dies ist natürlich nicht so trivial, aber eine Klasse, die den Start aller asynchronen Aufgaben steuert, ist möglich. Die Verwendung von Bare RX ist schwieriger.

Metriken


Es ist interessant zu sehen, wie viele parallele Abfragen durchschnittlich im Hintergrund ausgeführt werden . Mit Metriken können Sie die Ursache für Benutzerbeschwerden über Lethargie verstehen. Zumindest können wir die Ausführung im Hintergrund dessen, was wir nicht erwartet hatten, aus der Liste der Gründe ausschließen.

, , , , - - 10% . , .

Fazit


— , . «» . , , , , .

, , . — - , , — . — . , . . .

. Kotlin, . , .

AppsConf 2018, AppsConf 2019 . 38 : , Android, UX, , - , , Kotlin.

, youtube- 22–23 .

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


All Articles