Ich kenne RxJava sehr gut und bin kürzlich zu Kotlin Coroutines and Flow gewechselt.
RxKotlin ist im Grunde dasselbe wie RxJava, es fügt nur etwas syntaktischen Zucker hinzu, um das Schreiben von RxJava-Code in Kotlin komfortabler / idiomatischer zu gestalten.
Ein "fairer" Vergleich zwischen RxJava und Kotlin Coroutines sollte Flow in die Mischung aufnehmen und ich werde versuchen zu erklären, warum hier. Das wird ein bisschen lang, aber ich werde versuchen, es mit Beispielen so einfach wie möglich zu halten.
Mit RxJava haben Sie verschiedene Objekte (seit Version 2):
fun observeEventsA(): Observable<String>
fun observeEventsB(): Flowable<String>
fun encrypt(original: String): Single<String>
fun cached(key: String): Maybe<MyData>
fun syncPending(): Completable
In Kotlin Coroutines + Flow benötigen Sie nicht viele Entitäten. Wenn Sie keinen Ereignisstrom haben, können Sie einfach Coroutinen verwenden (Suspendierungsfunktionen):
fun observeEvents(): Flow<String>
suspend fun encrypt(original: String): String
suspend fun cached(key: String): MyData?
suspend fun syncPending()
Bonus: Kotlin Flow / Coroutines-Unterstützungswerte null
(Unterstützung mit RxJava 2 entfernt)
Was ist mit den Betreibern?
Mit RxJava haben Sie so viele Operatoren ( map
,filter
, flatMap
, switchMap
, ...), und für die meisten von ihnen gibt es eine Version für jeden Entitätstyp ( Single.map()
, Observable.map()
, ...).
Kotlin Coroutines + Flow benötigt nicht so viele Operatoren . Lassen Sie uns anhand einiger Beispiele zu den am häufigsten verwendeten Operatoren sehen, warum
Karte()
RxJava:
fun getPerson(id: String): Single<Person>
fun observePersons(): Observable<Person>
fun getPersonName(id: String): Single<String> {
return getPerson(id)
.map { it.firstName }
}
fun observePersonsNames(): Observable<String> {
return observePersons()
.map { it.firstName }
}
Kotlin Coroutinen + Flow
suspend fun getPerson(id: String): Person
fun observePersons(): Flow<Person>
suspend fun getPersonName(id: String): String? {
return getPerson(id).firstName
}
fun observePersonsNames(): Flow<String> {
return observePersons()
.map { it.firstName }
}
Sie benötigen keinen Operator für den "Einzelfall" und er ist für den Fall ziemlich ähnlich Flow
Fall .
flatMap ()
Angenommen, Sie müssen für jede Person aus einer Datenbank (oder einem Remote-Service) die Versicherung abrufen
RxJava
fun fetchInsurance(insuranceId: String): Single<Insurance>
fun getPersonInsurance(id: String): Single<Insurance> {
return getPerson(id)
.flatMap { person ->
fetchInsurance(person.insuranceId)
}
}
fun obseverPersonsInsurances(): Observable<Insurance> {
return observePersons()
.flatMap { person ->
fetchInsurance(person.insuranceId)
.toObservable()
}
}
Mal sehen mit Kotlin Coroutiens + Flow
suspend fun fetchInsurance(insuranceId: String): Insurance
suspend fun getPersonInsurance(id: String): Insurance {
val person = getPerson(id)
return fetchInsurance(person.insuranceId)
}
fun obseverPersonsInsurances(): Flow<Insurance> {
return observePersons()
.map { person ->
fetchInsurance(person.insuranceId)
}
}
Wie zuvor benötigen wir in dem einfachen Coroutine-Fall keine Operatoren. Wir schreiben den Code einfach so, als ob er nicht asynchron wäre, sondern verwenden nur Suspending-Funktionen.
Und Flow
da dies KEIN Tippfehler ist, ist kein flatMap
Operator erforderlich , wir können ihn nur verwenden map
. Und der Grund ist, dass Map Lambda eine Suspendierungsfunktion ist! Wir können Suspending Code darin ausführen !!!
Dafür brauchen wir keinen anderen Operator.
Für komplexere Aufgaben können Sie den Flow- transform()
Operator verwenden.
Jeder Flow-Operator akzeptiert eine Suspendierungsfunktion!
Wenn Sie also müssen, filter()
aber Ihr Filter einen Netzwerkanruf ausführen muss, können Sie!
fun observePersonsWithValidInsurance(): Flow<Person> {
return observerPersons()
.filter { person ->
val insurance = fetchInsurance(person.insuranceId)
insurance.isValid()
}
}
delay (), startWith (), concatWith (), ...
In RxJava gibt es viele Operatoren zum Anwenden von Verzögerungen oder zum Hinzufügen von Elementen vor und nach:
- verzögern()
- delaySubscription ()
- startWith (T)
- startWith (Observable)
- concatWith (...)
Mit Kotlin Flow können Sie einfach:
grabMyFlow()
.onStart {
delay(3000L)
emit("First item!")
emit(cachedItem())
}
.onEach { value ->
if (value.length() > 5) {
delay(1000L)
}
}
.onCompletion {
val endingSequence: Flow<String> = grabEndingSequence()
emitAll(endingSequence)
}
Fehlerbehandlung
RxJava hat viele Operatoren, um Fehler zu behandeln:
- onErrorResumeWith ()
- onErrorReturn ()
- onErrorComplete ()
Mit Flow brauchen Sie nicht viel mehr als den Operator catch()
:
grabMyFlow()
.catch { error ->
emit("We got an error: $error.message")
if (error is RecoverableError) {
emitAll(error.recover())
} else {
throw error
}
}
und mit Suspending-Funktion können Sie einfach verwenden try {} catch() {}
.
einfach zu schreibende Flow-Operatoren
Aufgrund der Coroutinen, die Flow unter der Haube antreiben, ist es viel einfacher, Operatoren zu schreiben. Wenn Sie jemals einen RxJava-Operator überprüft haben, werden Sie sehen, wie schwierig es ist und wie viele Dinge Sie lernen müssen.
Das Schreiben von Kotlin Flow-Operatoren ist einfacher. Sie können sich ein Bild machen, indem Sie sich den Quellcode der Operatoren ansehen, die hier bereits Teil von Flow sind . Der Grund dafür ist, dass Coroutinen das Schreiben von asynchronem Code erleichtern und die Verwendung von Operatoren nur natürlicher ist.
Als Bonus sind alle Flow-Operatoren alle Kotlin-Erweiterungsfunktionen. Dies bedeutet, dass Sie oder Bibliotheken einfach Operatoren hinzufügen können und sich nicht komisch anfühlen (z. B. observable.lift()
oder observable.compose()
).
Upstream-Thread leckt nicht Downstream
Was bedeutet das überhaupt?
Nehmen wir dieses RxJava-Beispiel:
urlsToCall()
.switchMap { url ->
if (url.scheme == "local") {
val data = grabFromMemory(url.path)
Flowable.just(data)
} else {
performNetworkCall(url)
.subscribeOn(Subscribers.io())
.toObservable()
}
}
.subscribe {
}
Wo ist also der Rückruf? subscribe
ausgeführt?
Die Antwort ist:
hängt davon ab...
Wenn es aus dem Netzwerk kommt, befindet es sich in einem E / A-Thread. Wenn es aus dem anderen Zweig stammt, ist es undefiniert. Dies hängt davon ab, welcher Thread zum Senden der URL verwendet wird.
Dies ist das Konzept des "stromaufwärtigen Gewindes, das stromabwärts austritt".
Bei Flow und Coroutines ist dies nicht der Fall, es sei denn, Sie benötigen dieses Verhalten ausdrücklich (using Dispatchers.Unconfined
).
suspend fun myFunction() {
withContext(Dispatchers.Main) {
urlsToCall()
.conflate()
.transform { url ->
if (url.scheme == "local") {
val data = grabFromMemory(url.path)
emit(data)
} else {
withContext(Dispatchers.IO) {
performNetworkCall(url)
}
}
}
.collect {
}
}
}
Coroutines-Code wird in dem Kontext ausgeführt, in dem sie ausgeführt wurden. Und nur der Teil mit dem Netzwerkaufruf wird auf dem E / A-Thread ausgeführt, während alles andere, was wir hier sehen, auf dem Hauptthread ausgeführt wird.
Nun, eigentlich wissen wir nicht, wo Code ausgeführt grabFromMemory()
wird. Wenn es sich um eine Suspendierungsfunktion handelt, wissen wir nur, dass er innerhalb des Hauptthreads aufgerufen wird, aber innerhalb dieser Suspendierungsfunktion könnte ein anderer Dispatcher verwendet werden, aber wann wird er verwendet Komm zurück mit dem Ergebnis, val data
das wird wieder im Haupt-Thread sein.
Wenn Sie sich einen Code ansehen, ist es einfacher zu erkennen, in welchem Thread er ausgeführt wird, wenn Sie einen expliziten Dispatcher sehen = es ist dieser Dispatcher, wenn Sie ihn nicht sehen: In welchem Thread-Dispatcher auch immer der Suspendierungsaufruf angezeigt wird wird gerufen.
Strukturierte Parallelität
Dies ist kein von Kotlin erfundenes Konzept, aber es ist etwas, das sie mehr als jede andere Sprache, die ich kenne, angenommen haben.
Wenn das, was ich hier erkläre, nicht ausreicht, lesen Sie diesen Artikel oder sehen Sie sich dieses Video an .
Also, was ist es?
Mit RxJava abonnieren Sie Observables und sie geben Ihnen ein Disposable
Objekt.
Sie müssen sich darum kümmern, es zu entsorgen, wenn es nicht mehr benötigt wird. Normalerweise behalten Sie also einen Verweis darauf (oder fügen ihn in ein CompositeDisposable
) ein, um ihn später aufzurufen dispose()
, wenn er nicht mehr benötigt wird. Wenn Sie dies nicht tun, gibt Ihnen der Linter eine Warnung.
RxJava ist etwas schöner als ein traditioneller Thread. Wenn Sie einen neuen Thread erstellen und etwas darauf ausführen, ist dies ein "Feuer und Vergessen". Sie haben nicht einmal die Möglichkeit, ihn abzubrechen: Thread.stop()
Ist veraltet, schädlich und die jüngste Implementierung führt tatsächlich nichts aus. Thread.interrupt()
Lässt Ihren Thread versagen usw. Alle Ausnahmen gehen verloren. Sie erhalten das Bild.
Mit Kotlin Coroutinen und Flow kehren sie das "Einweg" -Konzept um. Sie können keine Coroutine ohne ein erstellen CoroutineContext
.
Dieser Kontext definiert die scope
Ihrer Coroutine. Jede Kinder-Coroutine, die in dieser hervorgebracht wird, hat den gleichen Umfang.
Wenn Sie einen Flow abonnieren, müssen Sie sich in einer Coroutine befinden oder auch einen Bereich angeben.
Sie können weiterhin die Coroutinen, die Sie starten ( Job
) , referenzieren und abbrechen. Dadurch wird jedes Kind dieser Coroutine automatisch abgebrochen.
Wenn Sie ein Android-Entwickler sind, erhalten Sie diese Bereiche automatisch. Beispiel: viewModelScope
und Sie können Coroutinen in einem viewModel mit diesem Bereich starten, da Sie wissen, dass sie automatisch gelöscht werden, wenn das Ansichtsmodell gelöscht wird.
viewModelScope.launch {
}
Ein Teil des Bereichs wird beendet, wenn Kinder versagen, ein anderer Bereich lässt jedes Kind seinen eigenen Lebenszyklus verlassen, ohne andere Kinder zu stoppen, wenn eines ausfällt ( SupervisedJob
).
Warum ist das eine gute Sache?
Lassen Sie mich versuchen, es wie Roman Elizarov zu erklären getan hat.
Einige alte Programmiersprachen hatten dieses Konzept, mit goto
dem Sie nach Belieben von einer Codezeile zur nächsten springen können.
Sehr mächtig, aber wenn Sie missbraucht werden, kann dies zu sehr schwer verständlichem Code führen, der schwer zu debuggen und zu begründen ist.
Neue Programmiersprachen haben es schließlich vollständig aus der Sprache entfernt.
Wenn Sie if
oder while
oder when
es ist viel einfacher, über den Code nachzudenken: Egal, was in diesen Blöcken passiert, Sie werden irgendwann aus ihnen herauskommen, es ist ein "Kontext", Sie haben keine seltsamen Sprünge rein und raus .
Das Starten eines Threads oder das Abonnieren eines RxJava-Observable ähnelt dem goto: Sie führen Code aus, der so lange ausgeführt wird, bis "anderswo" gestoppt wird.
Wenn Sie bei Coroutinen die Angabe eines Kontexts / Bereichs verlangen, wissen Sie, dass Coroutinen nach Abschluss Ihres Kontexts abgeschlossen sind, wenn Ihr Kontext abgeschlossen ist, unabhängig davon, ob Sie einzelne Coroutinen oder 10 Tausend haben.
Sie können immer noch mit Coroutinen "gehen", indem Sie verwenden GlobalScope
, was Sie nicht aus dem gleichen Grund tun sollten, den Sie nicht goto
in Sprachen verwenden sollten, die es bereitstellen.
Irgendwelche Nachteile?
Flow befindet sich noch in der Entwicklung und einige Funktionen, die derzeit in RxJava verfügbar sind, sind in Kotlin Coroutines Flow noch nicht verfügbar.
Der große fehlt, gerade jetzt, ist share()
Betreiber und seine Freunde ( publish()
, replay()
etc ...)
Sie befinden sich derzeit in einem fortgeschrittenen Entwicklungsstadium und werden voraussichtlich bald (kurz nach dem bereits veröffentlichten Kotlin 1.4.0
) veröffentlicht. Das API-Design finden Sie hier :