Concurrency Mastery Review
Du prüfst, ob Nebenläufigkeit sicher, lebenszyklusbewusst und reaktionsschnell bleibt. So werden Apps stabiler.
Concurrency Mastery Review bedeutet: Du prüfst Nebenläufigkeit nicht nur darauf, ob der Code kompiliert, sondern ob er im echten App-Alltag sicher, lebenszyklusbewusst und reaktionsschnell bleibt. In modernen Android-Apps betrifft das fast jede Datenabfrage, jeden Flow, jedes ViewModel und jede Operation, die länger dauert als ein kurzer Zugriff auf Speicher.
Was ist das?
Ein Concurrency Mastery Review ist eine bewusste Qualitätsprüfung deines Coroutine-Designs. Du fragst: Läuft Arbeit dort, wo sie hingehört? Wird sie beendet, wenn der Nutzer den Bildschirm verlässt? Bleibt die Oberfläche bedienbar, während Daten geladen, verarbeitet oder gespeichert werden? Damit verbindest du Kotlin Coroutines, Flow, Jetpack-Architektur und Android-Performance zu einem praktischen Denkmodell.
Für Anfänger ist wichtig: Nebenläufigkeit ist nicht nur „mehrere Dinge gleichzeitig tun“. In Android bedeutet sie vor allem, die Hauptschleife der App zu schützen. Der Main Thread zeichnet die Oberfläche, verarbeitet Eingaben und hält Compose oder klassische Views aktuell. Wenn du dort blockierst, sieht der Nutzer Ruckler, verzögerte Klicks oder im schlimmsten Fall einen ANR-Dialog. Wenn du dagegen Arbeit falsch auslagerst, entstehen andere Fehler: Daten kommen zu spät, Jobs laufen weiter, obwohl niemand sie braucht, oder mehrere Operationen überschreiben sich gegenseitig.
Das Review ist deshalb kein Extra für große Teams. Es ist eine Gewohnheit, die du schon in kleinen Apps üben kannst. Du schaust nicht nur auf einzelne suspend-Funktionen, sondern auf den Weg einer Aufgabe: vom UI-Event über das ViewModel, weiter ins Repository, eventuell durch einen Flow, bis zurück zum UI-State.
Wie funktioniert es?
Baue dir ein mentales Modell aus drei Fragen: Wer startet die Arbeit? Wie lange darf sie leben? Auf welchem Thread oder Dispatcher soll sie laufen? Diese Fragen klingen schlicht, verhindern aber viele typische Fehler.
In einer Compose-App startet die UI möglichst keine langfristige Geschäftslogik direkt. Ein Button ruft eine ViewModel-Funktion auf, das ViewModel startet Arbeit in viewModelScope, und das Repository stellt suspend-Funktionen oder Flows bereit. So ist klar: Wenn das ViewModel verschwindet, werden seine laufenden Jobs abgebrochen. Das ist Lifecycle-Sicherheit.
Der passende Dispatcher ist die zweite Ebene. Netzwerk- und Datenbankbibliotheken sind oft bereits suspend-freundlich, aber CPU-lastige Arbeit gehört nicht auf den Main Thread. Für blockierende I/O-Arbeit nutzt du typischerweise Dispatchers.IO, für rechenintensive Verarbeitung Dispatchers.Default. Wichtig ist: Die Entscheidung sollte dort liegen, wo die Arbeit fachlich bekannt ist. Eine Repository-Funktion darf intern ihren Dispatcher wählen, statt die UI damit zu belasten.
Flow ergänzt das Modell, wenn Werte über Zeit entstehen: Suchergebnisse, Datenbankänderungen, Login-Zustände oder Fortschritt. Ein Flow ist kalt, solange ihn niemand sammelt. In der UI sammelst du ihn lebenszyklusbewusst, etwa mit collectAsStateWithLifecycle. Dadurch verarbeitet deine Oberfläche Daten nur dann aktiv, wenn sie sichtbar genug ist. Das spart Arbeit und reduziert Fehler durch veraltete UI-Zustände.
Responsiveness prüfst du nicht nur per Gefühl. Du beobachtest, ob Eingaben während einer Operation möglich bleiben, ob Ladezustände korrekt angezeigt werden und ob unnötige Mehrfacharbeit vermieden wird. Wenn ein Nutzer schnell hintereinander sucht, sollte nicht jede alte Anfrage später noch die neue Anzeige überschreiben. Hier helfen Operatoren wie debounce, distinctUntilChanged, flatMapLatest oder klare Job-Abbruchlogik. Du musst nicht jeden Operator auswendig kennen, aber du solltest erkennen, wann alte Arbeit ungültig geworden ist.
Safety bedeutet außerdem strukturierte Nebenläufigkeit. Starte nicht unkontrolliert globale Coroutines. GlobalScope ist in App-Code fast immer ein Warnsignal, weil der Job keinen sinnvollen Besitzer hat. Ein Job ohne Besitzer lässt sich schwer abbrechen, testen und erklären. Gute Coroutine-Strukturen machen sichtbar, welche Arbeit zu welchem Bildschirm, Use Case oder App-Prozess gehört.
In der Praxis
Stell dir eine Produktsuche vor. Der Nutzer tippt einen Suchbegriff, das ViewModel ruft ein Repository auf, und die UI zeigt Ladezustand, Ergebnis oder Fehler. Ein Review betrachtet hier nicht nur die API-Antwort. Es prüft, ob schnelle Eingaben alte Anfragen abbrechen, ob Fehler in UI-State übersetzt werden und ob die Sammlung im UI-Lebenszyklus hängt.
Kotlin-Beispiel:
data class SearchUiState(
val query: String = "",
val loading: Boolean = false,
val results: List<Product> = emptyList(),
val errorMessage: String? = null
)
class SearchViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val query = MutableStateFlow("")
val uiState: StateFlow<SearchUiState> =
query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { term ->
flow {
emit(SearchUiState(query = term, loading = true))
val results = repository.searchProducts(term)
emit(SearchUiState(query = term, results = results))
}.catch { error ->
emit(
SearchUiState(
query = term,
errorMessage = error.message ?: "Suche fehlgeschlagen"
)
)
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = SearchUiState()
)
fun onQueryChange(value: String) {
query.value = value
}
}
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
SearchContent(
state = state,
onQueryChange = viewModel::onQueryChange
)
}
Dieses Beispiel zeigt mehrere Review-Punkte. flatMapLatest sorgt dafür, dass eine alte Suche nicht mehr weiter zählt, wenn ein neuer Begriff kommt. stateIn macht aus dem Flow einen stabilen UI-State im ViewModel. SharingStarted.WhileSubscribed verhindert, dass die Suche unbegrenzt aktiv bleibt, wenn keine UI mehr sammelt. collectAsStateWithLifecycle bindet Compose an den Lebenszyklus, statt den Flow dauerhaft zu sammeln.
Eine typische Stolperfalle ist das Sammeln eines Flows direkt in einer Composable mit einer frei gestarteten Coroutine. Wenn diese Coroutine nicht sauber an den Lebenszyklus gebunden ist, kann sie weiterlaufen oder mehrfach starten. In Compose solltest du für UI-State bevorzugt lebenszyklusbewusste APIs nutzen. Für einzelne Seiteneffekte gibt es LaunchedEffect, aber auch dort musst du den Key bewusst wählen. Ein falscher Key startet Arbeit bei jeder recomposition neu oder hält veraltete Werte fest.
Eine zweite Stolperfalle ist fehlende Fehlerbegrenzung. Wenn eine Exception in einer Coroutine nicht abgefangen wird, kann sie den Scope abbrechen und dadurch weitere Arbeit stoppen. Im Review fragst du deshalb: Wo wird ein Fehler in einen UI-Zustand übersetzt? Wo darf ein Fehler den gesamten Ablauf abbrechen? Wo brauchst du eine Retry-Strategie, und wo reicht eine klare Fehlermeldung?
Eine dritte Stolperfalle ist Schein-Performance. Eine App wirkt nicht automatisch reaktionsschnell, nur weil du launch benutzt. Wenn du innerhalb der Coroutine CPU-lastige Arbeit auf dem Main Thread ausführst, blockierst du weiterhin die Oberfläche. Prüfe bei auffälligen Rucklern mit Profiler, Logs oder Debugger, welcher Thread die Arbeit ausführt. In Code-Reviews kannst du gezielt nach Schleifen, JSON-Verarbeitung, Bildmanipulation, Sortierung großer Listen oder blockierenden Bibliotheksaufrufen suchen.
Als Entscheidungsregel kannst du dir merken: UI ruft Absichten auf, ViewModel besitzt UI-nahe Arbeit, Repository kapselt Datenarbeit, und jeder längere Ablauf braucht einen klaren Besitzer. Wenn du diesen Besitzer nicht benennen kannst, ist dein Coroutine-Design wahrscheinlich zu lose.
Zum Testen reicht nicht nur ein erfolgreicher Standardfall. Prüfe auch: Was passiert, wenn der Nutzer den Bildschirm sofort verlässt? Was passiert bei zwei schnellen Suchbegriffen? Wird der Ladezustand zurückgesetzt? Kommt ein Fehler als UI-State an? Für ViewModels kannst du Coroutine-Testwerkzeuge nutzen, Test-Dispatcher einsetzen und Flow-Emissionen gezielt prüfen. In manuellen Tests hilft dir der Debugger: Setze Breakpoints in ViewModel und Repository, rotiere den Bildschirm oder navigiere zurück, und beobachte, ob Jobs erwartbar enden.
Fazit
Concurrency Mastery Review macht aus Coroutines, Flow und Lifecycle keine lose Sammlung von APIs, sondern ein prüfbares Qualitätskriterium für echte Android-Apps. Wenn du deinen nächsten Bildschirm baust, verfolge eine längere Aufgabe bewusst vom Klick bis zum Ergebnis: Benenne den Scope, prüfe den Dispatcher, beobachte den Abbruch bei Navigation, teste schnelle Nutzeraktionen und bespreche im Code-Review, ob die App dabei sicher und reaktionsschnell bleibt.