13. Apr. 2025·6 Min. Lesezeit

Kotlin Coroutines vs RxJava für Networking und Hintergrundarbeit

Kotlin Coroutines vs RxJava: Vergleiche Cancellation, Fehlerbehandlung und Testmuster für Networking und Hintergrundaufgaben in echten Android-Apps.

Kotlin Coroutines vs RxJava für Networking und Hintergrundarbeit

Warum diese Entscheidung für produktives Networking wichtig ist

Networking und Hintergrundarbeit in einer echten Android-App sind mehr als ein einzelner API-Aufruf. Dazu gehören Login und Token-Refresh, Bildschirme, die während des Ladens gedreht werden können, Syncs nachdem der Nutzer einen Bildschirm verlässt, Foto-Uploads und periodische Arbeiten, die den Akku nicht leer saugen dürfen.

Die Bugs, die am meisten weh tun, sind selten Syntaxprobleme. Sie zeigen sich, wenn asynchrone Arbeit länger lebt als die UI (Lecks), wenn Cancellation die UI anhält, aber die eigentliche Anfrage weiterläuft (verschwendeter Traffic und hängende Spinner), wenn Retries Anfragen multiplizieren (Rate-Limits, Sperren), oder wenn verschiedene Schichten Fehler unterschiedlich behandeln, so dass niemand vorhersagen kann, was der Nutzer sieht.

Die Entscheidung Kotlin Coroutines vs RxJava beeinflusst die tägliche Zuverlässigkeit:

  • Wie du Arbeit modellierst (Einer-auf-einmal-Aufrufe vs Streams)
  • Wie Cancellation propagiert wird
  • Wie Fehler repräsentiert und der UI angezeigt werden
  • Wie du Threads für Netzwerk, Disk und UI kontrollierst
  • Wie testbar Timing, Retries und Randfälle sind

Die Muster unten konzentrieren sich auf das, was unter Last oder auf langsamen Netzen typischerweise bricht: Cancellation, Fehlerbehandlung, Retries und Timeouts sowie Testgewohnheiten, die Regressionen verhindern. Beispiele bleiben kurz und praktisch.

Kernmodelle: suspend-Aufrufe, Streams und Flow

Der Hauptunterschied zwischen Kotlin Coroutines vs RxJava ist die Form der Arbeit, die du modellierst.

Eine suspend-Funktion repräsentiert eine einmalige Operation. Sie liefert einen Wert zurück oder wirft einen Fehler. Das passt zu den meisten Netzwerkaufrufen: Profil holen, Einstellungen aktualisieren, Foto hochladen. Der aufrufende Code liest von oben nach unten, was auch nach Logging, Caching oder Branching überschaubar bleibt.

RxJava fragt zuerst, ob du mit einem Wert oder vielen Werten über die Zeit arbeitest. Ein Single ist ein einmaliges Ergebnis (Erfolg oder Fehler). Ein Observable (oder Flowable) ist ein Stream, der viele Werte senden kann, dann beendet oder fehlschlägt. Das passt zu wirklich ereignisartigen Features: Texteingabe-Events, WebSocket-Nachrichten oder Polling.

Flow ist die coroutine-freundliche Art, einen Stream darzustellen. Denk daran als die „Stream-Version“ von Coroutines, mit strukturierter Cancellation und direkter Integration in suspend-APIs.

Eine schnelle Faustregel:

  • Nutze suspend für eine einzelne Anfrage und eine Antwort.
  • Nutze Flow für Werte, die sich über die Zeit ändern.
  • Nutze RxJava, wenn deine App stark auf Operatoren und komplexe Stream-Zusammenstellungen angewiesen ist.

Wenn Features wachsen, leidet die Lesbarkeit meist zuerst, wenn du ein Stream-Modell erzwingst für einen Einmal-Aufruf oder fortlaufende Events wie einen einzelnen Rückgabewert behandelst. Passe die Abstraktion zuerst an die Realität an und baue dann Konventionen darum.

Cancellation in der Praxis (mit kurzen Codebeispielen)

Cancellation ist der Punkt, an dem sich asynchroner Code entweder sicher anfühlt oder zu zufälligen Abstürzen und verschwendeten Aufrufen wird. Das Ziel ist einfach: Wenn der Nutzer einen Bildschirm verlässt, sollte jede dafür gestartete Arbeit stoppen.

Bei Kotlin Coroutines ist Cancellation ins Modell eingebaut. Ein Job repräsentiert Arbeit, und mit strukturierter Concurrency gibst du Jobs normalerweise nicht herum. Du startest Arbeit in einem Scope (z. B. ViewModel-Scope). Wenn dieser Scope abgebrochen wird, wird alles darin ebenfalls abgebrochen.

class ProfileViewModel(
    private val api: Api
) : ViewModel() {

    fun loadProfile() = viewModelScope.launch {
        // If the ViewModel is cleared, this coroutine is cancelled,
        // and so is the in-flight network call (if the client supports it).
        val profile = api.getProfile() // suspend
        // update UI state here
    }
}

Zwei produktionsrelevante Details sind wichtig:

  • Führe suspend-Netzwerkaufrufe über einen stornierbaren Client aus. Andernfalls stoppt die Coroutine, aber der HTTP-Call kann weiterlaufen.
  • Verwende withTimeout (oder withTimeoutOrNull) für Anfragen, die nicht hängen dürfen.

RxJava nutzt explizites Disposal. Du hältst ein Disposable für jede Subscription oder sammelst sie in einem CompositeDisposable. Wenn der Bildschirm verschwindet, rufst du dispose auf und die Kette sollte stoppen.

class ProfilePresenter(private val api: ApiRx) {
    private val bag = CompositeDisposable()

    fun attach() {
        bag += api.getProfile()
            .subscribe(
                { profile -> /* render */ },
                { error -> /* show error */ }
            )
    }

    fun detach() {
        bag.clear() // cancels in-flight work if upstream supports cancellation
    }
}

Eine praktische Regel beim Verlassen eines Bildschirms: Wenn du nicht zeigen kannst, wo Cancellation passiert (Scope-Cancellation oder dispose()), geh davon aus, dass die Arbeit weiterläuft, und behebe das, bevor du auslieferst.

Fehlerbehandlung, die verständlich bleibt

Ein großer Unterschied in Kotlin Coroutines vs RxJava ist, wie Fehler reisen. Coroutines lassen Fehler wie normalen Code aussehen: Ein suspend-Aufruf wirft, und der Aufrufer entscheidet, was zu tun ist. Rx leitet Fehler durch den Stream — das ist mächtig, aber leicht, Probleme zu verstecken, wenn du nicht aufpasst.

Verwende Exceptions für unerwartete Fehler (Timeouts, 500er, Parsing-Fehler). Modellier Fehler als Daten, wenn die UI eine spezifische Antwort braucht (falsches Passwort, „E-Mail bereits verwendet“) und das Teil deines Domain-Modells sein soll.

Ein einfaches Coroutine-Muster bewahrt den Stacktrace und bleibt lesbar:

suspend fun loadProfile(): Profile = try {
    api.getProfile() // may throw
} catch (e: IOException) {
    throw NetworkException("No connection", e)
}

runCatching und Result sind nützlich, wenn du wirklich Erfolg oder Fehler zurückgeben willst, ohne zu werfen:

suspend fun loadProfileResult(): Result<Profile> =
    runCatching { api.getProfile() }

Sei vorsichtig mit getOrNull(), wenn du den Fehler nicht zusätzlich behandelst. Das kann reale Bugs leise in einen „Empty State“-Bildschirm verwandeln.

In RxJava halte den Fehlerpfad ebenfalls explizit. Nutze onErrorReturn nur für sichere Fallbacks. onErrorResumeNext ist sinnvoll, wenn du die Quelle wechseln musst (z. B. zu gecachten Daten). Für Retries halte die Regeln eng mit retryWhen, damit du nicht z. B. bei „falschem Passwort“ einfach erneut versuchst.

Gewohnheiten, die geschluckte Fehler verhindern:

  • Logge oder melde einen Fehler einmal, nah an der Stelle, wo du Kontext hast.
  • Bewahre die Original-Exception als cause, wenn du wrappst.
  • Vermeide Catch-All-Fallbacks, die jeden Fehler in einen Default-Wert verwandeln.
  • Mache nutzerrelevante Fehler zu einem typisierten Modell, nicht zu Strings.

Threading-Basics: Dispatchers vs Schedulers

Iteriere ohne Rewrite
Entwirf deinen Netzwerk-Workflow und Bildschirme, und generiere bei Bedarf sauber neu, wenn sich Specs ändern.
Jetzt bauen

Viele asynchrone Bugs hängen mit Threads zusammen: schwere Arbeit auf dem Main-Thread oder UI-Updates von einem Hintergrund-Thread. Kotlin Coroutines vs RxJava unterscheiden sich hauptsächlich darin, wie du Thread-Wechsel ausdrückst.

Bei Coroutines startest du oft auf dem Main-Thread für UI-Arbeit und springst dann zu einem Hintergrund-Dispatcher für teure Teile. Übliche Optionen sind:

  • Dispatchers.Main für UI-Updates
  • Dispatchers.IO für blockierende I/O wie Netzwerk und Disk
  • Dispatchers.Default für CPU-Arbeit wie JSON-Parsing, Sortieren, Verschlüsselung

Ein einfaches Muster ist: Daten holen, off-main parsen, dann rendern.

viewModelScope.launch(Dispatchers.Main) {
    val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
    val profile = withContext(Dispatchers.Default) { parseProfile(json) }
    _uiState.value = UiState.Content(profile)
}

RxJava drückt "wo Arbeit passiert" mit subscribeOn aus und "wo Ergebnisse beobachtet werden" mit observeOn. Eine häufige Überraschung ist zu erwarten, dass observeOn das Upstream beeinflusst — tut es nicht. subscribeOn setzt den Thread für die Quelle und Operatoren darüber, und jedes observeOn wechselt den Thread ab dem Punkt.

api.fetchProfileJson()
    .subscribeOn(Schedulers.io())
    .map { json -> parseProfile(json) } // still on io unless you change it
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        { profile -> render(profile) },
        { error -> showError(error) }
    )

Eine Regel, die Überraschungen vermeidet: Halte UI-Arbeit an einem Ort. Bei Coroutines aktualisiere oder sammle UI-State auf Dispatchers.Main. Bei RxJava setze ein finales observeOn(main) direkt vor dem Rendern und verstreue nicht unnötig observeOn-Aufrufe.

Wenn ein Bildschirm ruckelt, verschiebe zuerst Parsing und Mapping vom Main-Thread. Diese eine Änderung behebt viele reale Probleme.

Timeouts, Retries und parallele Arbeit für Netzwerkaufrufe

Muster in eine App umsetzen
Erstelle eine produktionsreife Android-App mit generiertem Kotlin-Code und klaren Backend-APIs.
AppMaster ausprobieren

Der Happy Path ist selten das Problem. Probleme entstehen durch hängende Calls, Retries, die alles schlimmer machen, oder „parallele“ Arbeit, die gar nicht parallel läuft. Diese Muster entscheiden oft, ob ein Team Kotlin Coroutines vs RxJava bevorzugt.

Timeouts, die schnell fehlschlagen

Mit Coroutines kannst du einen harten Deckel um jeden suspend-Aufruf legen. Halte das Timeout nahe beim Aufruf, damit du die richtige UI-Nachricht zeigen kannst.

val user = withTimeout(5_000) {
    api.getUser() // suspend
}

In RxJava hängst du den timeout-Operator an den Stream. Das ist nützlich, wenn das Timeout-Verhalten Teil einer gemeinsamen Pipeline sein soll.

Retries ohne Schaden anzurichten

Retry nur, wenn Retry sicher ist. Eine einfache Regel: Retry idempotente Requests (wie GET) freier als Requests mit Seiteneffekten (z. B. "create order"). Begrenze die Anzahl und füge Delay oder Jitter hinzu.

Gute Standard-Guardrails:

  • Retry bei Netzwerkausfällen und temporären Serverfehlern.
  • Nicht bei Validierungsfehlern (400er) oder Auth-Fehlern.
  • Begrenze Retries (oft 2–3) und logge das finale Scheitern.
  • Verwende Backoff-Delays, damit du den Server nicht bombardierst.

In RxJava lässt retryWhen ausdrücken: "Retry nur bei diesen Fehlern mit dieser Verzögerung." In Coroutines hat Flow retry und retryWhen, während einfache suspend-Funktionen oft eine kleine Schleife mit delay verwenden.

Parallele Aufrufe ohne verknoteten Code

Coroutines machen parallele Arbeit direkt: starte zwei Anfragen, warte auf beide.

coroutineScope {
    val profile = async { api.getProfile() }
    val feed = async { api.getFeed() }
    profile.await() to feed.await()
}

RxJava glänzt, wenn das Kombinieren mehrerer Quellen der eigentliche Zweck der Kette ist. zip ist üblich für "warte auf beide", merge, wenn du Ergebnisse sofort sehen willst.

Bei großen oder schnellen Streams zählt Backpressure weiterhin. RxJava's Flowable hat ausgereifte Backpressure-Werkzeuge. Coroutines Flow löst viele Fälle gut, aber du brauchst eventuell Buffer- oder Drop-Strategien, wenn Events deine UI oder DB-Writes überflügeln.

Interop- und Migrationsmuster (gemischte Codebasen)

Die meisten Teams wechseln nicht über Nacht. Eine praktische Kotlin Coroutines vs RxJava Migration hält die App stabil, während du Modul für Modul umstellst.

Eine Rx-API in eine suspend-Funktion einhüllen

Wenn du ein vorhandenes Single<T> oder Completable hast, hülle es mit Cancellation-Unterstützung, so dass eine gecancelte Coroutine die Rx-Subscription disposed.

suspend fun <T : Any> Single<T>.awaitCancellable(): T =
  suspendCancellableCoroutine { cont ->
    val d = subscribe(
      { value -> cont.resume(value) {} },
      { error -> cont.resumeWithException(error) }
    )
    cont.invokeOnCancellation { d.dispose() }
  }

Das vermeidet eine häufige Fehlerquelle: Der Nutzer verlässt den Bildschirm, die Coroutine wird abgebrochen, aber der Netzwerkaufruf läuft weiter und aktualisiert später gemeinsamen Zustand.

Coroutine-Code für Rx-Aufrufer bereitstellen

Während der Migration erwarten einige Schichten noch Rx-Typen. Wickele suspend-Arbeit in Single.fromCallable und blocke nur auf einem Hintergrund-Thread.

fun loadProfileRx(api: Api): Single<Profile> =
  Single.fromCallable {
    runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
  }

Halte diese Grenze klein und dokumentiert. Für neuen Code ruf die suspend-API direkt aus einem Coroutine-Scope auf.

Wo Flow passt und wo nicht

Flow kann viele Observable-Use-Cases ersetzen: UI-State, Datenbank-Updates und Paging-ähnliche Streams. Es ist weniger direkt, wenn du stark auf hot streams, Subjects, fortgeschrittene Backpressure-Tuning oder viele kundenspezifische Operatoren angewiesen bist, die dein Team bereits kennt.

Eine Migrationsstrategie, die Verwirrung minimiert:

  • Konvertiere Leaf-Module zuerst (Netzwerk, Storage) zu suspend-APIs.
  • Füge kleine Adapter an Modulgrenzen hinzu (Rx → suspend, suspend → Rx).
  • Ersetze Rx-Streams durch Flow nur, wenn du auch die Konsumenten kontrollierst.
  • Halte pro Featurebereich eine Async-Variante.
  • Lösche Adapter, sobald der letzte Caller migriert ist.

Testmuster, die du wirklich nutzt

Eine Stack für UI und API
Baue Web- und Mobile-Clients neben deinem Backend, damit Fehlerbehandlung konsistent bleibt.
App erstellen

Timing- und Cancellation-Probleme sind Orte, an denen asynchrone Bugs sich verstecken. Gute Async-Tests machen Zeit deterministisch und Ergebnisse leicht prüfbar. Auch hier fühlt sich Kotlin Coroutines vs RxJava unterschiedlich an, obwohl beides gut testbar ist.

Coroutines: runTest, TestDispatcher und Zeitkontrolle

Für Coroutine-Code bevorzuge runTest mit einem Test-Dispatcher, damit dein Test nicht von echten Threads oder Delays abhängt. Virtuelle Zeit lässt dich Timeouts, Retries und Debounce-Fenster auslösen, ohne zu schlafen.

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `emits Loading then Success`() = runTest {
  val dispatcher = StandardTestDispatcher(testScheduler)
  val repo = Repo(api = fakeApi, io = dispatcher)

  val states = mutableListOf<UiState>()
  val job = launch(dispatcher) { repo.loadProfile().toList(states) }

  testScheduler.runCurrent() // run queued work
  assert(states.first() is UiState.Loading)

  testScheduler.advanceTimeBy(1_000) // trigger delay/retry windows
  testScheduler.runCurrent()
  assert(states.last() is UiState.Success)

  job.cancel()
}

Um Cancellation zu testen, breche das sammelnde Job (oder den Parent-Scope) ab und prüfe, dass deine Fake-API stoppt oder keine weiteren Zustände mehr emittiert werden.

RxJava: TestScheduler, TestObserver, deterministische Zeit

Rx-Tests kombinieren üblicherweise einen TestScheduler für Zeit und einen TestObserver für Assertions.

@Test
fun `disposes on cancel and stops emissions`() {
  val scheduler = TestScheduler()
  val observer = TestObserver<UiState>()

  val d = repo.loadProfileRx(scheduler)
    .subscribeWith(observer)

  scheduler.triggerActions()
  observer.assertValueAt(0) { it is UiState.Loading }

  d.dispose()
  scheduler.advanceTimeBy(1, TimeUnit.SECONDS)
  observer.assertValueCount(1) // no more events after dispose
}

Beim Testen von Fehlerpfaden in beiden Stilen fokussiere dich auf das Mapping, nicht auf den exakten Exception-Typ. Prüfe den erwarteten UI-Zustand nach 401, Timeout oder fehlerhafter Antwort.

Ein kleiner Satz Checks deckt die meisten Regressionen ab:

  • Loading- und Endzustände (Success, Empty, Error)
  • Cancellation-Aufräumen (Job gecancelt, Disposable disposed)
  • Error-Mapping (Server-Codes → Nutzer-Meldungen)
  • Keine doppelten Emissionen nach Retries
  • Zeitbasierte Logik mit virtueller Zeit, nicht echten Delays

Häufige Fehler, die Produktionsbugs verursachen

Die meisten Produktionsprobleme entstehen nicht durch die Wahl Kotlin Coroutines vs RxJava. Sie kommen von ein paar Gewohnheiten, die Arbeit länger laufen lassen, als du denkst, sie doppelt ausführen oder die UI zur falschen Zeit berühren.

Ein häufiger Leak entsteht, wenn Arbeit im falschen Scope gestartet wird. Startest du einen Netzwerkaufruf in einem Scope, das länger lebt als der Bildschirm (oder du erstellst ein eigenes Scope und brichst es nie ab), kann die Anfrage nach dem Verlassen des Bildschirms fertig werden und trotzdem Zustand aktualisieren. Bei Coroutines zeigt sich das oft als standardmäßig verwendeter langlebiger Scope; bei RxJava ist es meist ein vergessenes dispose.

Ein anderer Klassiker ist "fire and forget". Globale Scopes und vergessene Disposables fühlen sich okay an, bis die Arbeit sich anhäuft. Ein Chat-Bildschirm, der bei jedem Resume refreshed, kann nach ein paar Navigationsschritten mehrere Refresh-Jobs gleichzeitig laufen haben, die jeweils Speicher halten und um Netzwerk konkurrieren.

Retr ies sind auch leicht falsch zu machen. Unbegrenzte Retries oder ohne Delay können dein Backend zuspammen und Akku leersaugen. Besonders gefährlich bei permanenten Fehlern wie 401 nach Logout. Mache Retries konditional, mit Backoff, und stoppe, wenn der Fehler nicht behebbar ist.

Threading-Fehler führen zu Abstürzen, die schwer zu reproduzieren sind. Du könntest JSON auf dem Main-Thread parsen oder UI von einem Hintergrund-Thread updaten, je nachdem, wo du einen Dispatcher oder Scheduler gesetzt hast.

Schnelle Checks, die die meisten Probleme erkennen:

  • Binde Arbeit an einen Lifecycle-Owner und cancelle, wenn dieser endet.
  • Mache Aufräumen offensichtlich: cancel Jobs oder clear Disposables an einer Stelle.
  • Setze strikte Limits für Retries (Anzahl, Delay, welche Fehler gelten).
  • Durchsetze in Code-Reviews eine Regel: UI-Updates nur auf dem Main-Thread.
  • Betrachte Hintergrund-Sync als System mit Einschränkungen, nicht als zufälligen Funktionsaufruf.

Wenn du Android-Apps aus generiertem Kotlin-Code auslieferst (z. B. von AppMaster), gelten dieselben Fallstricke. Du brauchst weiterhin klare Konventionen für Scopes, Cancellation, Retry-Limits und Thread-Regeln.

Schnelle Checkliste: Coroutines, RxJava oder beides wählen

Schnell zu einer funktionierenden Basis
Komm vom Rx- vs Coroutines-Diskurs zu einer funktionierenden App-Architektur, die du ausliefern kannst.
Projekt starten

Beginne mit der Form der Arbeit. Die meisten Netzwerkaufrufe sind einmalig, aber Apps haben auch laufende Signale wie Konnektivität, Auth-Status oder Live-Updates. Die falsche Abstraktion früh führt später meist zu unordentlicher Cancellation und schwer lesbaren Fehlerpfaden.

Eine einfache Entscheidungsregel (und wie du sie dem Team erklärst):

  • Einmalige Anfrage (Login, Profil holen): suspend bevorzugen.
  • Laufender Stream (Events, DB-Updates): Flow oder Rx Observable bevorzugen.
  • UI-Lifecycle-Cancellation: Coroutines in viewModelScope oder lifecycleScope sind oft einfacher als manuelle Disposables.
  • Starke Nutzung fortgeschrittener Stream-Operatoren und Backpressure: RxJava kann besser passen, besonders in älteren Codebasen.
  • Komplexe Retries und Error-Mapping: Wähle die Lösung, die dein Team lesbar halten kann.

Eine praktische Regel: Wenn ein Bildschirm eine Anfrage macht und ein Ergebnis rendert, halten Coroutines den Code nahe an einer normalen Funktionsaufrufstruktur. Wenn du eine Pipeline vieler Events baust (Tippen, Debounce, vorherige Anfragen abbrechen, Filter kombinieren), fühlt sich RxJava oder Flow natürlicher an.

Konsistenz schlägt Perfektion. Zwei gute Muster, die überall verwendet werden, sind leichter zu warten als fünf „beste“ Muster, die inkonsistent genutzt werden.

Beispielszenario: Login, Profilabruf und Hintergrund-Sync

Technische Schulden früh vermeiden
Erhalte echten Quellcode, den du prüfen, erweitern und warten kannst, wenn sich Anforderungen ändern.
Code generieren

Ein häufiger Produktionsfluss: Der Nutzer tippt Login, du rufst ein Auth-Endpoint, danach holst du das Profil für den Home-Screen und startest schließlich einen Hintergrund-Sync. Hier kann sich Kotlin Coroutines vs RxJava im Alltag unterschiedlich anfühlen.

Coroutines-Version (sequenziell + cancellable)

Mit Coroutines ist die "mach dies, dann das"-Form natürlich. Wenn der Nutzer den Bildschirm schließt, stoppt das Abbrechen des Scopes die laufenden Arbeiten.

suspend fun loginAndLoadProfile(): Result<Profile> = runCatching {
  val token = api.login(email, password) // suspend
  val profile = api.profile("Bearer $token")
  syncManager.startSyncInBackground(token) // fire-and-forget
  profile
}.recoverCatching { e ->
  throw when (e) {
    is HttpException -> when (e.code()) {
      401 -> AuthExpiredException()
      in 500..599 -> ServerDownException()
      else -> e
    }
    is IOException -> NoNetworkException()
    else -> e
  }
}

// UI layer
val job = viewModelScope.launch { loginAndLoadProfile() }
override fun onCleared() { job.cancel() }

RxJava-Version (Kette + Disposal)

In RxJava ist derselbe Ablauf eine Kette. Cancellation heißt hier dispose, typischerweise mit einem CompositeDisposable.

val d = api.login(email, password)
  .flatMap { token -> api.profile("Bearer $token").map { it to token } }
  .doOnSuccess { (_, token) -> syncManager.startSyncInBackground(token) }
  .onErrorResumeNext { e: Throwable ->
    Single.error(
      when (e) {
        is HttpException -> if (e.code() == 401) AuthExpiredException() else e
        is IOException -> NoNetworkException()
        else -> e
      }
    )
  }
  .subscribe({ (profile, _) -> show(profile) }, { showError(it) })

compositeDisposable.add(d)
override fun onCleared() { compositeDisposable.clear() }

Eine minimale Testsuite sollte hier drei Ergebnisse abdecken: Erfolg, gemappte Fehler (401, 500er, kein Netz) und Cancellation/Disposal.

Nächste Schritte: Konventionen wählen und beibehalten

Teams geraten meist in Schwierigkeiten, weil Muster über Features hinweg variieren, nicht weil Kotlin Coroutines vs RxJava per se falsch ist. Eine kurze Entscheidungs-Notiz (auch eine Seite reicht) spart Zeit bei Reviews und macht Verhalten vorhersehbar.

Beginne mit einer klaren Trennung: Einmalige Arbeit (ein Netzwerkaufruf, der einmal zurückgibt) vs Streams (Updates über Zeit, z. B. WebSocket, Standort, DB-Änderungen). Entscheide den Default für jede Kategorie und definiere, wann Ausnahmen erlaubt sind.

Füge dann ein kleines Set geteilter Helfer hinzu, damit jedes Feature gleich reagiert, wenn das Netzwerk zickt:

  • Ein Ort, um Fehler (HTTP-Codes, Timeouts, Offline) in App-Level-Failures zu mappen, die die UI versteht
  • Standard-Timeouts für Netzwerkaufrufe mit klarer Möglichkeit zum Überschreiben für lange Operationen
  • Eine Retry-Policy, die sagt, was sicher wiederholt werden kann (z. B. GET vs POST)
  • Eine Cancellation-Regel: Was stoppt beim Verlassen eines Bildschirms, was darf weiterlaufen
  • Logging-Regeln, die Support helfen, ohne sensitive Daten zu leakern

Testkonventionen sind genauso wichtig. Einigt euch auf einen Standard, damit Tests nicht von echter Zeit oder echten Threads abhängen. Für Coroutines bedeutet das typischerweise ein Test-Dispatcher und strukturierte Scopes. Für RxJava bedeutet das TestScheduler und explizites Disposal. Ziel: schnelle, deterministische Tests ohne Sleeps.

Wenn du insgesamt schneller werden willst, ist AppMaster (appmaster.io) eine Option, um Backend-APIs und Kotlin-basierte Mobile-Apps zu generieren, statt alles von Hand zu schreiben. Selbst bei generiertem Code gelten dieselben Produktionskonventionen für Cancellation, Fehler, Retries und Tests, um Netzwer kverhalten vorhersehbar zu halten.

FAQ

Wann sollte ich eine `suspend`-Funktion vs. einen Stream für Networking verwenden?

Default für eine einzelne Anfrage, die einmal zurückgibt, wie Login oder Profilabfrage: suspend. Verwende Flow (oder Rx-Streams), wenn sich Werte über die Zeit ändern, z. B. WebSocket-Nachrichten, Konnektivität oder Datenbank-Updates.

Stoppt Coroutine-Cancellation wirklich eine laufende Netzwerkanfrage?

Ja — aber nur, wenn dein HTTP-Client Abbruch unterstützt. Coroutines beenden die Coroutine, wenn die Scope abgebrochen wird; der darunterliegende HTTP-Call muss ebenfalls stornierbar sein, sonst läuft die Anfrage im Hintergrund weiter.

Was ist der sicherste Weg, Leaks zu verhindern, wenn der Nutzer einen Bildschirm verlässt?

Binde Arbeit an eine Lifecycle-Scope wie viewModelScope, so dass sie automatisch abgebrochen wird, wenn die Logik des Bildschirms endet. Vermeide globale oder langlebige Scopes, sofern die Arbeit nicht wirklich app-weit ist.

Wie sollte sich die Fehlerbehandlung zwischen Coroutines und RxJava unterscheiden?

Bei Coroutines werfen Fehler typischerweise Ausnahmen, die du mit try/catch nahe der Stelle behandelst, wo du sie in UI-Zustände übersetzen kannst. In RxJava reisen Fehler durch den Stream — halte diesen Pfad explizit und vermeide Operatoren, die Fehler still in Default-Werte verwandeln.

Sollte ich Fehler als Exceptions oder als Daten modellieren?

Verwende Ausnahmen für unerwartete Fehler wie Timeouts, 500er oder Parsing-Probleme. Wenn die UI eine konkrete Antwort braucht (z. B. „falsches Passwort“), nutze typisierte Fehlerdaten, statt auf String-Matching zu setzen.

Wie füge ich Timeouts einfach hinzu, ohne den Code unübersichtlich zu machen?

Lege das Timeout nahe an den Aufruf, damit du die richtige UI-Meldung zeigen kannst. Bei Coroutines ist withTimeout einfach für suspend-Aufrufe; in RxJava macht der timeout-Operator das Timeout zum Teil der Kette.

Wie implementiere ich Retries, ohne doppelte Anfragen oder Sperren zu riskieren?

Retry nur, wenn es sicher ist — meist für idempotente Requests wie GET. Begrenze die Anzahl (z. B. 2–3), vermeide Retries bei Validierungs- oder Auth-Fehlern und füge Verzögerung/Jitter hinzu, damit du Server und Akku nicht belastest.

Was ist die häufigste Threading-Falle bei Dispatchers vs Schedulers?

Der Klassiker: Parsing auf dem Main-Thread oder UI-Updates von Hintergrund-Threads. Bei Coroutines nutzt du Dispatchers (Main, IO, Default), in RxJava subscribeOn und observeOn. Sorge dafür, dass das Rendering immer auf dem Main-Thread passiert.

Kann ich RxJava und Coroutines während einer Migration mischen?

Ja — aber halte die Grenze klein und cancellation-aware. Wickele Rx in suspend mit einem Adapter, der die Rx-Subscription bei Coroutine-Abbruch disposed, und exponiere suspend-APIs an Rx nur dort, wo wirklich nötig.

Wie teste ich Cancellation, Retries und zeitbasiertes Verhalten verlässlich?

Nutze virtuelle Zeit, damit Tests nicht schlafen müssen oder von echten Threads abhängen. Bei Coroutines nimm runTest mit einem Test-Dispatcher; bei RxJava verwende TestScheduler und prüfe, dass nach dispose() keine Emissionen mehr kommen.

Einfach zu starten
Erschaffe etwas Erstaunliches

Experimentieren Sie mit AppMaster mit kostenlosem Plan.
Wenn Sie fertig sind, können Sie das richtige Abonnement auswählen.

Starten