Coroutinen sind ein leistungsstarkes Tool für die asynchrone Codeausführung. Sie arbeiten parallel, kommunizieren miteinander und verbrauchen nur wenige Ressourcen. Es scheint, dass ohne Angst Coroutinen in die Produktion eingeführt werden können. Aber es gibt Ängste und sie stören.
Vladimir Ivanovs Bericht auf
AppsConf besagt, dass der Teufel nicht so schlimm ist und dass Sie jetzt Coroutinen verwenden können:
Über den Redner : Vladimir Ivanov (
dzigoro ) ist ein führender Android-Entwickler bei
EPAM mit 7 Jahren Erfahrung,
mag Lösungsarchitektur, React Native und iOS-Entwicklung und verfügt über ein
Google Cloud Architect- Zertifikat.
Alles, was Sie lesen, ist ein Produkt aus Erfahrungswerten und verschiedenen Studien. Nehmen Sie es also so, wie es ist, ohne jegliche Garantie.
Coroutinen, Kotlin und RxJava
Zur Information: Der aktuelle Status von Corutin befindet sich in der Version, Beta verlassen.
Kotlin 1.3 wurde veröffentlicht, Coroutinen werden für stabil erklärt und es gibt Frieden auf der Welt.

Ich habe kürzlich auf Twitter eine Umfrage durchgeführt, bei der Benutzer von Coroutine:
- 13% der Coroutinen in Lebensmitteln. Alles ist gut;
- 25% probieren sie im Haustierprojekt aus;
- 24% - Was ist Kotlin?
- Der Großteil von 38% RxJava ist überall.
Statistiken sind nicht glücklich. Ich glaube, dass
RxJava ein zu komplexes Tool für Aufgaben ist, in denen es häufig von Entwicklern verwendet wird. Coroutinen eignen sich besser zur Steuerung des asynchronen Betriebs.
In meinen vorherigen Berichten habe ich darüber gesprochen, wie man von RxJava zu Coroutinen in Kotlin umgestaltet, daher werde ich nicht im Detail darauf eingehen, sondern nur an die Hauptpunkte erinnern.
Warum verwenden wir Coroutinen?
Denn wenn wir RxJava verwenden, sehen die üblichen Implementierungsbeispiele folgendermaßen aus:
interface ApiClientRx { fun login(auth: Authorization) : Single<GithubUser> fun getRepositories (reposUrl: String, auth: Authorization) : Single<List<GithubRepository>> }
Wir haben eine Schnittstelle, zum Beispiel schreiben wir einen GitHub-Client und möchten einige Operationen dafür ausführen:
- Benutzer anmelden.
- Holen Sie sich eine Liste der GitHub-Repositorys.
In beiden Fällen geben Funktionen einzelne Geschäftsobjekte zurück: GitHubUser oder eine Liste von GitHubRepository.
Der Implementierungscode für diese Schnittstelle lautet wie folgt:
private fun attemptLoginRx () { showProgress(true) compositeDisposable.add(apiClient.login(auth) .flatMap { user -> apiClient.getRepositories(user.repos_url, auth) } .map { list -> list.map { it.full_name } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doFinally { showProgress(false) } .subscribe( { list -> showRepositories(this, list) }, { error -> Log.e("TAG", "Failed to show repos", error) } )) }
- Wir nehmen
CompositeDisposable, damit kein Speicherverlust auftritt.
- Fügen Sie der ersten Methode einen Aufruf hinzu.
- Wir verwenden bequeme Operatoren, um den Benutzer zu erreichen, zum Beispiel
flatMap .
- Wir erhalten eine Liste der Repositories.
- Wir schreiben eine
Boilerplate so, dass sie auf den richtigen Threads
läuft .
- Wenn alles fertig ist, zeigen wir die Liste der Repositorys für den angemeldeten Benutzer.
RxJava-Code-Schwierigkeiten:- Komplexität Meiner Meinung nach ist der Code zu kompliziert für die einfache Aufgabe, zwei Netzwerkanrufe durchzuführen und etwas auf der Benutzeroberfläche anzuzeigen.
- Ungebundene Stapelspuren. Stapelspuren hängen fast nicht mit dem Code zusammen, den Sie schreiben.
- Ressourcen ausgeben . RxJava generiert viele Objekte unter der Haube und die Leistung kann abnehmen.
Was ist der gleiche Code mit Coroutinen bis Version 0.26?Bei 0,26 hat sich die API geändert, und wir sprechen über die Produktion. Bisher hat es noch niemand geschafft, 0,26 im Produkt anzuwenden, aber wir arbeiten daran.
Mit Coroutinen wird sich unsere Benutzeroberfläche erheblich ändern . Die Funktionen geben keine Singles und andere Hilfsobjekte mehr zurück. Sie geben sofort Geschäftsobjekte zurück: GitHubUser und eine Liste von GitHubRepository. Die Funktionen GitHubUser und GitHubRepository verfügen über
Suspend- Modifikatoren. Das ist gut so, denn Suspend verpflichtet uns fast zu nichts:
interface ApiClient { suspend fun login(auth: Authorization) : GithubUser suspend fun getRepositories (reposUrl: String, auth: Authorization) : List<GithubRepository> }
Wenn Sie sich den Code ansehen, der die Implementierung dieser Schnittstelle bereits verwendet, wird sich dieser im Vergleich zu RxJava erheblich ändern:
private fun attemptLogin () { launch(UI) { val auth = BasicAuthorization(login, pass) try { showProgress(true) val userlnfo = async { apiClient.login(auth) }.await() val repoUrl = userlnfo.repos_url val list = async { apiClient.getRepositories(repoUrl, auth) }.await() showRepositories( this, list.map { it -> it.full_name } ) } catch (e: RuntimeException) { showToast("Oops!") } finally { showProgress(false) } } }
- Die Hauptaktion findet statt, bei der wir den
Coroutine Builder als asynchron bezeichnen , auf eine Antwort warten und
userlnfo erhalten .
- Wir verwenden Daten von diesem Objekt.
- Machen Sie einen weiteren
asynchronen Anruf und
warten Sie .
Alles sieht so aus, als ob keine asynchrone Arbeit stattfindet, und wir schreiben einfach die Befehle in die Spalte und sie werden ausgeführt. Am Ende tun wir, was auf der Benutzeroberfläche zu tun ist.
Warum sind Coroutinen besser?- Dieser Code ist leichter zu lesen. Es ist so geschrieben, als ob es konsistent wäre.
- Höchstwahrscheinlich ist die Leistung dieses Codes besser als bei RxJava.
- Es ist sehr einfach, Tests zu schreiben, aber wir werden etwas später darauf zurückkommen.
2 Schritte zur Seite
Lassen Sie uns ein wenig abschweifen, es gibt ein paar Dinge, die noch besprochen werden müssen.
Schritt 1. withContext vs launch / async
Zusätzlich zum
asynchronen Coroutine Builder gibt es den
Coroutine Builder withContext .
Durch Starten oder
Asynchronisieren wird ein neuer
Coroutine-Kontext erstellt , was nicht immer erforderlich ist. Wenn Sie einen Coroutine-Kontext haben, den Sie in der gesamten Anwendung verwenden möchten, müssen Sie ihn nicht neu erstellen. Sie können eine vorhandene einfach wiederverwenden. Dazu benötigen Sie einen Coroutine Builder mit Kontext. Der vorhandene Coroutine-Kontext wird einfach wiederverwendet. Es wird 2-3 mal schneller sein, aber jetzt ist es eine prinzipienlose Frage. Wenn die genauen Zahlen interessant sind, dann ist
hier die Frage zum
Stapelüberlauf mit Benchmarks und Details.
Allgemeine Regel: Verwenden Sie withContext ohne Zweifel dort, wo es semantisch passt. Wenn Sie jedoch parallel laden müssen, z. B. mehrere Bilder oder Daten, können Sie zwischen asynchron und warten wählen.
Schritt 2. Refactoring
Was ist, wenn Sie eine wirklich komplexe RxJava-Kette umgestalten? Ich bin in der Produktion darauf gestoßen:
observable1.getSubject().zipWith(observable2.getSubject(), (t1, t2) -> {
Ich hatte eine komplizierte Kette mit einem
öffentlichen Thema , mit
Reißverschluss und
Nebenwirkungen in jedem
Reißverschluss , die etwas anderes zum
Eventbus schickten. Die Aufgabe bestand zumindest darin, den Eventbus loszuwerden. Ich saß einen Tag da, konnte aber den Code nicht umgestalten, um das Problem zu lösen.
Die richtige Entscheidung stellte sich heraus, alles rauszuwerfen und den Code auf Coroutine in 4 Stunden neu zu schreiben .
Der folgende Code ist dem, was ich bekommen habe, sehr ähnlich:
try { val firstChunkJob = async { call1 } val secondChunkJob = async { call2 } val thirdChunkJob = async { call3 } return Result( firstChunkJob.await(), secondChunkJob.await(), thirdChunkJob.await()) } catch (e: Exception) {
- Wir machen Async für eine Aufgabe, für die zweite und dritte.
- Wir warten auf das Ergebnis und fügen alles in ein Objekt ein.
- Fertig!
Wenn Sie komplexe Ketten haben und es Coroutinen gibt, dann überarbeiten Sie einfach. Es ist sehr schnell.
Was hindert Entwickler daran, Coroutinen in Prod zu verwenden?
Meiner Meinung nach werden wir als Entwickler derzeit nur aus Angst vor etwas Neuem daran gehindert, Coroutinen zu verwenden:
- Wir wissen nicht, was wir mit dem Lebenszyklus , der Aktivität und dem Fragmentlebenszyklus anfangen sollen. Wie arbeite ich in diesen Fällen mit Coroutinen?
- Es gibt keine Erfahrung in der Lösung alltäglicher komplexer Aufgaben in der Produktion mit Corutin.
- Nicht genug Werkzeuge. Für RxJava wurde eine Reihe von Bibliotheken und Funktionen geschrieben. Zum Beispiel RxFCM . RxJava selbst hat viele Operatoren, was gut ist, aber was ist mit Coroutine?
- Wir verstehen nicht wirklich, wie man Coroutinen testet.
Wenn wir diese vier Ängste loswerden, können wir nachts ruhig schlafen und Coroutinen in der Produktion verwenden.
Lassen Sie uns Punkt für Punkt.
1. Lebenszyklusmanagement
- Coroutinen können als Einweg- oder AsyncTask auslaufen . Dieses Problem muss manuell gelöst werden.
- Um zufällige Nullzeigerausnahmen zu vermeiden , müssen Coroutinen gestoppt werden.
Hör auf
Kennen Sie
Thread.stop () ? Wenn Sie es benutzt haben, dann nicht lange. In
JDK 1.1 wurde die Methode sofort für veraltet erklärt, da es unmöglich ist, einen bestimmten Code zu übernehmen und zu stoppen, und es keine Garantie dafür gibt, dass er korrekt ausgeführt wird. Höchstwahrscheinlich erhalten Sie nur
Speicherbeschädigungen .
Daher
funktioniert Thread.stop () nicht . Die Stornierung muss kooperativ sein, dh der Code auf der anderen Seite, um zu wissen, dass Sie sie stornieren.
Wie wenden wir Stopps mit RxJava an:
private val compositeDisposable = CompositeDisposable() fun requestSmth() { compositeDisposable.add( apiClientRx.requestSomething() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(result -> {}) } override fun onDestroy() { compositeDisposable.dispose() }
In RxJava verwenden wir
CompositeDisposable .
- Fügen Sie der Aktivität im Fragment oder im Presenter, in dem wir RxJava verwenden, die Variable
CompositeDisposable hinzu.
-
Fügen Sie in
onDestro y
Dispose hinzu und alle Ausnahmen
verschwinden von selbst.
Ungefähr das gleiche Prinzip bei Coroutinen:
private val job: Job? = null fun requestSmth() { job = launch(UI) { val user = apiClient.requestSomething() … } } override fun onDestroy() { job?.cancel() }
Betrachten Sie ein Beispiel für eine
einfache Aufgabe .
In der Regel geben
Coroutine-Builder einen
Job zurück und werden in einigen Fällen zurückgestellt.
- Wir können uns diesen Job merken.
- Geben Sie den Befehl
"Launch" Coroutine Builder . Der Prozess beginnt, etwas passiert, das Ergebnis der Ausführung wird gespeichert.
- Wenn wir nichts anderes übergeben, startet "Starten" die Funktion und gibt uns einen Link zum Job zurück.
- Job wird erinnert, und in onDestroy sagen wir
"Abbrechen" und alles funktioniert gut.
Was ist das Problem des Ansatzes? Jeder Job braucht ein Feld. Sie müssen eine Liste von Jobs führen, um sie alle zusammen abzubrechen. Der Ansatz führt zu einer Duplizierung des Codes. Tun Sie dies nicht.
Die gute Nachricht ist, dass wir
Alternativen haben :
CompositeJob und
Lifecycle-fähiger Job .
CompositeJob ist ein Analogon von CompositeDisposable. Es sieht ungefähr so aus
: private val job: CompositeJob = CompositeJob() fun requestSmth() { job.add(launch(UI) { val user = apiClient.requestSomething() ... }) } override fun onDestroy() { job.cancel() }
- Für ein Fragment starten wir einen Job.
- Wir setzen alle
Jobs in CompositeJob und geben den Befehl:
"job.cancel () für alle!" .
Der Ansatz lässt sich leicht in 4 Zeilen implementieren, wobei die Klassendeklaration nicht berücksichtigt wird:
Class CompositeJob { private val map = hashMapOf<String, Job>() fun add(job: Job, key: String = job.hashCode().toString()) = map.put(key, job)?.cancel() fun cancel(key: String) = map[key]?.cancel() fun cancel() = map.forEach { _ ,u -> u.cancel() } }
Sie benötigen:
-
Karte mit einem String-Schlüssel,
- Methode
hinzufügen , in der Sie Job hinzufügen,
- optionaler
Schlüsselparameter .
Wenn Sie denselben Schlüssel für denselben Job verwenden möchten, bitte. Wenn nicht, löst
hashCode unser Problem. Fügen Sie den Auftrag zur Karte hinzu, die wir übergeben haben, und brechen Sie den vorherigen mit demselben Schlüssel ab. Wenn wir die Aufgabe übererfüllen, interessiert uns das vorherige Ergebnis nicht. Wir stornieren es und fahren es wieder.
Abbrechen ist einfach: Wir erhalten den Auftrag per Schlüssel und stornieren. Der zweite Abbruch für die gesamte Karte bricht alles ab. Der gesamte Code wird in einer halben Stunde in vier Zeilen geschrieben und funktioniert. Wenn Sie nicht schreiben möchten, nehmen Sie das obige Beispiel.
Lebenszyklusbewusster Job
Haben Sie
Android Lifecycle ,
Lifecycle-Besitzer oder
Beobachter verwendet ?

Unsere
Aktivitäten und
Fragmente haben bestimmte Zustände. Highlights:
erstellt, gestartet und
fortgesetzt . Es gibt verschiedene Übergänge zwischen Zuständen.
Mit LifecycleObserver können Sie diese Übergänge abonnieren und etwas tun, wenn einer der Übergänge auftritt.
Es sieht ganz einfach aus:
public class MyObserver implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void connectListener() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) public void disconnectListener() { … } }
Sie legen die Annotation mit einigen Parametern für die Methode auf und sie wird mit dem entsprechenden Übergang aufgerufen. Verwenden Sie einfach diesen Ansatz für Coroutine:
class AndroidJob(lifecycle: Lifecycle) : Job by Job(), LifecycleObserver { init { lifecycle.addObserver(this) } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun destroy() { Log.d("AndroidJob", "Cancelling a coroutine") cancel() } }
- Sie können die Basisklasse
AndroidJob schreiben.
- Wir übertragen den
Lebenszyklus in die Klasse.
- Die
LifecycleObserver- Schnittstelle implementiert den Job.
Alles was wir brauchen:
- Fügen Sie im Konstruktor Lifecycle als Observer hinzu.
- Abonnieren Sie
ON_DESTROY oder alles andere, was uns interessiert.
- Abbrechen in ON_DESTROY.
-
Holen Sie sich einen
ParentJob in Ihr Fragment.
- Rufen Sie den Konstruktor
Joy Jobs oder den
Lebenszyklus Ihres Aktivitätsfragments auf. Egal.
-
Übergeben Sie diesen
parentJob als
übergeordneten Job .
Der fertige Code sieht folgendermaßen aus:
private var parentJob = AndroidJob(lifecycle) fun do() { job = launch(UI, parent = parentJob) {
Wenn Sie übergeordnete Elemente abbrechen, werden alle untergeordneten Coroutinen abgebrochen, und Sie müssen nichts mehr in das Fragment schreiben. Alles geschieht automatisch, nicht mehr ON_DESTROY. Die Hauptsache
, vergessen Sie nicht,
parent = parentJob zu übergeben .
Wenn Sie verwenden, können Sie eine einfache Flusenregel schreiben, die Sie hervorhebt: "Oh, Sie haben Ihre Eltern vergessen!"
Mit
Lebenszyklusmanagement aussortiert. Wir haben einige Tools, mit denen Sie dies einfach und bequem erledigen können.
Was ist mit komplexen Szenarien und nicht trivialen Aufgaben in der Produktion?
2. Komplexe Anwendungsfälle
Komplexe Szenarien und nicht triviale Aufgaben sind:
-
Operatoren - Komplexe Operatoren in RxJava: flatMap, Debounce usw.
- Fehlerbehandlung
- komplexe Fehlerbehandlung. Nicht nur
try..catch , sondern zum Beispiel verschachtelt.
-
Caching ist eine nicht triviale Aufgabe. In der Produktion stießen wir auf einen Cache und wollten ein Tool, mit dem wir das Caching-Problem mit Coroutinen einfach lösen können.
Wiederholen
Als wir über die Operatoren für Coroutine
nachdachten , war die erste Option
repeatWhen () .
Wenn etwas schief gelaufen ist und Corutin den Server im Inneren nicht erreichen konnte, möchten wir es mehrmals mit einem exponentiellen Fallback erneut versuchen. Möglicherweise liegt der Grund in einer schlechten Verbindung, und wir erzielen das gewünschte Ergebnis, indem wir den Vorgang mehrmals wiederholen.
Mit Coroutinen ist diese Aufgabe einfach zu implementieren:
suspend fun <T> retryDeferredWithDelay( deferred: () -> Deferred<T>, tries: Int = 3, timeDelay: Long = 1000L ): T { for (i in 1..tries) { try { return deferred().await() } catch (e: Exception) { if (i < tries) delay(timeDelay) else throw e } } throw UnsupportedOperationException() }
Bedienerimplementierung:
- Er nimmt
aufgeschoben .
- Sie müssen
asynchron aufrufen, um dieses Objekt zu erhalten.
- Anstelle von
Zurückgestellt können Sie sowohl einen Suspend-Block als auch im Allgemeinen jede
Suspend-Funktion übergeben.- Die
for- Schleife - Sie warten auf das Ergebnis Ihrer Coroutine. Wenn etwas passiert und der Wiederholungszähler nicht erschöpft ist, versuchen Sie es erneut mit
Delay . Wenn nicht, dann nein.
Die Funktion kann einfach angepasst werden: Setzen Sie eine exponentielle Verzögerung oder übergeben Sie eine Lambda-Funktion, die die Verzögerung abhängig von den Umständen berechnet.
Verwenden Sie es, es funktioniert!
Reißverschlüsse
Wir begegnen ihnen auch oft. Auch hier ist alles einfach:
suspend fun <T1, T2, R> zip( source1: Deferred<T1>, source2: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zipper.apply(sourcel.await(), source2.await()) } suspend fun <T1, T2, R> Deferred<T1>.zipWith( other: Deferred<T2>, zipper: BiFunction<T1, T2, R>): R { return zip(this, other, zipper) }
- Verwenden Sie den
Reißverschluss und warten Sie auf Ihren Deferred.
- Anstelle von "Zurückgestellt" können Sie die Funktion "Suspend" und den Coroutine Builder mit withContext verwenden. Sie vermitteln den Kontext, den Sie benötigen.
Das funktioniert wieder und ich hoffe, dass ich diese Angst beseitigt habe.
Cache
Haben Sie eine Cache-Implementierung in der Produktion mit RxJava? Wir verwenden RxCache.

Im Diagramm links:
View und
ViewModel . Rechts sind die Datenquellen: Netzwerkanrufe und die Datenbank.
Wenn etwas zwischengespeichert werden soll, ist der Cache eine weitere Datenquelle.
Arten von Cache:
- Netzwerkquelle für Netzwerkanrufe.
- In-Memory-Cache .
- Permanenter Cache mit Ablauf, der auf der Festplatte gespeichert werden soll, damit der Cache den Neustart der Anwendung überlebt.
Schreiben wir einen einfachen und primitiven
Cache für den dritten Fall. Der Coroutine Builder withContext kommt wieder zur Rettung.
launch(UI) { var data = withContext(dispatcher) { persistence.getData() } if (data == null) { data = withContext(dispatcher) { memory.getData() } if (data == null) { data = withContext(dispatcher) { network.getData() } memory.cache(url, data) persistence.cache(url, data) } } }
- Sie führen jede Operation mit withContext aus und prüfen, ob Daten eingehen.
- Wenn die Daten aus der
Persistenz nicht kommen, versuchen Sie, sie aus
memory.cache abzurufen .
- Wenn auch kein memory.cache vorhanden ist, wenden Sie sich an die
Netzwerkquelle und rufen Sie Ihre Daten ab. Vergessen Sie natürlich nicht, alle Caches einzutragen.
Dies ist eine eher primitive Implementierung und es gibt viele Fragen, aber die Methode funktioniert, wenn Sie einen Cache an einem Ort benötigen. Für Produktionsaufgaben reicht dieser Cache nicht aus. Es wird etwas Komplizierteres benötigt.
Rx hat RxCache
Für diejenigen, die noch RxJava verwenden, können Sie RxCache verwenden. Wir benutzen es auch noch.
RxCache ist eine spezielle Bibliothek. Ermöglicht das Zwischenspeichern von Daten und das Verwalten des Lebenszyklus.
Sie möchten beispielsweise sagen, dass diese Daten nach 15 Minuten ablaufen: "Bitte senden Sie nach dieser Zeit keine Daten aus dem Cache, sondern senden Sie mir neue Daten."
Die Bibliothek ist insofern wunderbar, als sie das Team deklarativ unterstützt. Die Erklärung ist sehr ähnlich zu dem, was Sie mit
Retrofit machen :
public interface FeatureConfigCacheProvider { @ProviderKey("features") @LifeCache(duration = 15, timeUnit = TimeUnit.MINUTES) fun getFeatures( result: Observable<Features>, cacheName: DynamicKey ): Observable<Reply<Features>> }
- Sie sagen, dass Sie einen
CacheProvider haben .
- Starten Sie eine Methode und sagen Sie, dass die
LifeCache- Lebensdauer 15 Minuten
beträgt . Der Schlüssel, mit dem es verfügbar sein wird, ist
Funktionen .
- Gibt
Observable <Reply zurück , wobei
Reply ein Hilfsbibliotheksobjekt für die Arbeit mit dem Cache ist.
Die Verwendung ist recht einfach:
val restObservable = configServiceRestApi.getFeatures() val features = featureConfigCacheProvider.getFeatures( restObservable, DynamicKey(CACHE_KEY) )
-
Greifen Sie über den Rx-Cache auf
RestApi zu .
-
Wenden Sie sich an
CacheProvider .
- Füttere ihn mit einem Observable.
- Die Bibliothek selbst wird herausfinden, was zu tun ist: Gehen Sie in den Cache oder nicht. Wenn die Zeit abläuft, wenden Sie sich an
Observable und führen Sie einen weiteren Vorgang aus.
Die Nutzung der Bibliothek ist sehr praktisch und ich möchte eine ähnliche für Coroutine erhalten.
Coroutine Cache in Entwicklung
In EPAM schreiben wir die
Coroutine-Cache- Bibliothek, die alle Funktionen von RxCache ausführt. Wir haben die erste Version geschrieben und innerhalb des Unternehmens ausgeführt. Sobald die erste Veröffentlichung herauskommt, werde ich sie gerne auf meinem Twitter veröffentlichen. Es wird so aussehen:
val restFunction = configServiceRestApi.getFeatures() val features = withCache(CACHE_KEY) { restFunction() }
Wir werden eine Suspend-Funktion
getFeatures haben . Wir werden die Funktion als Block an eine spezielle Funktion höherer Ordnung mit
Cache übergeben , die
herausfindet , was zu tun ist.
Vielleicht machen wir die gleiche Schnittstelle, um deklarative Funktionen zu unterstützen.
Fehlerbehandlung

Eine einfache Fehlerbehandlung wird häufig von Entwicklern gefunden und normalerweise ganz einfach gelöst. Wenn Sie keine komplizierten Dinge haben, fangen Sie in catch eine
Ausnahme ab und sehen sich an, was dort passiert ist, schreiben in das Protokoll oder zeigen dem Benutzer einen Fehler an. Auf der Benutzeroberfläche können Sie dies problemlos tun.
In einfachen Fällen ist alles erwartungsgemäß einfach - die Fehlerbehandlung mit Coroutinen erfolgt durch
try-catch-finally .
In der Produktion gibt es neben einfachen Fällen:
- Verschachtelter
Versuch ,
- Viele verschiedene Arten von
Ausnahmen ,
- Fehler im Netzwerk oder in der Geschäftslogik,
- Benutzerfehler. Er hat wieder etwas falsch gemacht und war für alles verantwortlich.
Darauf müssen wir vorbereitet sein.
Es gibt zwei Lösungen:
CoroutineExceptionHandler und den Ansatz mit
Ergebnisklassen .
Coroutine-Ausnahmebehandlungsroutine
Dies ist eine spezielle Klasse für die Behandlung komplexer Fehlerfälle.
Mit ExceptionHandler können Sie Ihre
Ausnahme als Argument als Fehler betrachten und damit umgehen.
Wie gehen wir normalerweise mit komplexen Fehlern um?
Der Benutzer drückte etwas, die Taste funktionierte nicht. Er muss sagen, was schief gelaufen ist, und es auf eine bestimmte Aktion hinweisen: Überprüfen Sie das Internet, WLAN, versuchen Sie es später oder löschen Sie die Anwendung und verwenden Sie sie nie wieder. Dies dem Benutzer zu sagen ist ganz einfach:
val handler = CoroutineExceptionHandler(handler = { , error -> hideProgressDialog() val defaultErrorMsg = "Something went wrong" val errorMsg = when (error) { is ConnectionException -> userFriendlyErrorMessage(error, defaultErrorMsg) is HttpResponseException -> userFriendlyErrorMessage(Endpoint.EndpointType.ENDPOINT_SYNCPLICITY, error) is EncodingException -> "Failed to decode data, please try again" else -> defaultErrorMsg } Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show() })
- Lassen Sie uns die Standardmeldung erhalten: "Etwas ist schief gelaufen!" und analysieren Sie die Ausnahme.
- Wenn dies eine
ConnectionException ist, nehmen wir eine lokalisierte Nachricht aus den Ressourcen: „Mann, schalten Sie Wi-Fi ein und Ihre Probleme werden verschwinden. Ich garantiere es. "
- Wenn der
Server etwas Falsches gesagt hat , müssen Sie dem Client mitteilen: "Abmelden und erneut anmelden" oder "Tun Sie dies nicht in Moskau, tun Sie es in einem anderen Land" oder "Entschuldigung, Genosse. Ich kann nur sagen, dass etwas schief gelaufen ist. “
- Wenn dies ein völlig
anderer Fehler ist , zum Beispiel
aus dem Gedächtnis heraus , sagen wir: "Es ist etwas schiefgegangen, es tut mir leid."
- Alle Meldungen werden angezeigt.
Was Sie in den
CoroutineExceptionHandler schreiben, wird auf demselben
Dispatcher ausgeführt, auf dem Sie die Coroutine ausführen. Wenn Sie daher den Befehl zum Starten der Benutzeroberfläche eingeben, geschieht alles auf der Benutzeroberfläche. Sie benötigen keinen separaten
Versand, was sehr praktisch ist.
Die Verwendung ist einfach:
launch(uiDispatcher + handler) { ... }
Es gibt einen
Plus- Operator. Fügen Sie im Coroutine-Kontext einen
Handler hinzu, und alles funktioniert, was sehr praktisch ist. Wir haben das eine Weile benutzt.
Ergebnisklassen
Später stellten wir fest, dass der CoroutineExceptionHandler möglicherweise fehlt. Das Ergebnis, das durch die Arbeit von Coroutine entsteht, kann aus mehreren Daten aus verschiedenen Teilen bestehen oder mehrere Situationen verarbeiten.
Der Ansatz der
Ergebnisklassen hilft, dieses Problem zu lösen:
sealed class Result { data class Success(val payload: String) : Result() data class Error(val exception: Exception) : Result() }
- In Ihrer Geschäftslogik starten Sie eine
Ergebnisklasse .
- Als
versiegelt markieren .
- Sie erben von der Klasse zwei weitere Datenklassen:
Erfolg und
Fehler .
- Übertragen Sie in
Success Ihre Daten, die als Ergebnis der Coroutine-Ausführung generiert wurden.
—
Error exception.
- :
override suspend fun doTask(): Result = withContext(CommonPool) { if ( !isSessionValidForTask() ) { return@withContext Result.Error(Exception()) } … try { Result.Success(restApi.call()) } catch (e: Exception) { Result.Error(e) } }
Coroutine context — Coroutine builder withContex .
, :
— , error. .
— RestApi -.
— ,
Result.Success .
— ,
Result.Error .
- , ExceptionHandler .
Result classes , . Result classes, ExceptionHandler try-catch.
3.
, .
unit- , , . unit-.
, . , unit-, 2 :
- Replacing context . , ;
- Mocking coroutines . .
Replacing context
presenter:
val login() { launch(UI) { … } }
,
login , UI-. , ,
. , , unit-.
:
val login (val coroutineContext = UI) { launch(coroutineContext) { ... } }
— login coroutineContext. , . Kotlin , UI .
— Coroutine builder Coroutine Contex, .
unit- :
fun testLogin() { val presenter = LoginPresenter () presenter.login(Unconfined) }
—
LoginPresenter login - , , Unconfined.
—
Unconfined , , . .
Mocking coroutines
— .
Mockk unit-. unit- Kotlin, . suspend-
coEvery -.
login
githubUser :
coEvery { apiClient.login(any()) } returns githubUser
Mockito-kotlin , — . , , :
given { runBlocking { apiClient.login(any()) } }.willReturn (githubUser)
runBlocking .
given- , .
Presenter :
fun testLogin() { val githubUser = GithubUser('login') val presenter = LoginPresenter(mockApi) presenter.login (Unconfined) assertEquals(githubUser, presenter.user()) }
— -, ,
GitHubUser .
— LoginPresenter API, . .
—
presenter.login Unconfined , Presenter , .
Und alle! .
- Rx- . . , RxJava RxJava. - — , .
- . , . Unit- — , , , . — welcome!
- . , , , , . .
Nützliche Links
Nachrichten
30 Mail.ru . , .
AppsConf , .
, , , .
youtube- AppsConf 2018 — :)