Android Coden
Android 7 min lesen

supervisorScope in Kotlin Coroutines

supervisorScope hilft dir, unabhängige Coroutine-Aufgaben getrennt fehlschlagen zu lassen. So bleibt deine Android-App robuster.

Wenn du in einer Android-App mehrere Aufgaben parallel startest, stellt sich schnell eine wichtige Frage: Soll ein einzelner Fehler sofort alles abbrechen, oder dürfen andere Aufgaben weiterlaufen? supervisorScope ist genau für Fälle gedacht, in denen unabhängige Kind-Coroutines eigene Fehler haben dürfen, ohne automatisch ihre Geschwister zu beenden.

Was ist das?

supervisorScope ist eine Coroutine-Funktion aus Kotlin, die einen begrenzten Bereich mit Supervisor-Verhalten erzeugt. Innerhalb dieses Bereichs kannst du mehrere Kind-Coroutines starten. Wenn eine dieser Kind-Coroutines mit einer Exception scheitert, werden die anderen Kind-Coroutines nicht automatisch abgebrochen. Das ist der Kern: partial failure, also teilweise Fehler, bei independent children, also voneinander unabhängigen Kind-Aufgaben.

Das unterscheidet sich vom normalen coroutineScope. In einem regulären Scope gilt strukturierte Nebenläufigkeit mit strenger Fehlerweitergabe: Scheitert ein Kind, wird der gesamte Scope abgebrochen. Dieses Verhalten ist oft richtig, zum Beispiel wenn mehrere Teilschritte gemeinsam ein einziges Ergebnis bilden. Wenn aber jede Aufgabe für sich stehen kann, ist ein kompletter Abbruch unnötig streng.

Im Android-Kontext triffst du dieses Problem häufig beim Laden von UI-Daten. Ein Bildschirm kann zum Beispiel Profilinformationen, empfohlene Inhalte und einen Hinweisbanner parallel laden. Wenn der Banner nicht geladen werden kann, soll das Profil trotzdem erscheinen. supervisorScope erlaubt dir, diese fachliche Unabhängigkeit auch im Coroutine-Modell auszudrücken.

Wichtig ist die gedankliche Grenze: supervisorScope bedeutet nicht, dass Fehler verschwinden. Es bedeutet nur, dass ein Fehler nicht automatisch alle Geschwister mitnimmt. Du musst weiterhin entscheiden, wie jede einzelne Kind-Coroutine mit Fehlern umgeht. Genau dadurch wird der Code robuster, aber auch etwas anspruchsvoller.

Wie funktioniert es?

Das mentale Modell ist ein Team von parallelen Aufgaben unter einer Aufsicht. Der Supervisor beendet nicht sofort das ganze Team, nur weil eine einzelne Aufgabe scheitert. Die fehlgeschlagene Aufgabe bleibt fehlgeschlagen, aber andere Aufgaben dürfen weiterarbeiten. Der äußere Scope wartet trotzdem auf seine Kinder, denn supervisorScope bleibt Teil der strukturierten Nebenläufigkeit.

Das ist für Android wichtig, weil Coroutines oft an Lebenszyklen gekoppelt sind. In einem ViewModel startest du Arbeit typischerweise in viewModelScope. Dieser Scope wird abgebrochen, wenn das ViewModel gelöscht wird. supervisorScope hebt diese Lebenszyklusbindung nicht auf. Wenn der äußere Scope abgebrochen wird, werden auch die Kinder im supervisorScope abgebrochen. Supervisor-Verhalten betrifft vor allem Fehler einzelner Kinder, nicht die Lebensdauer des übergeordneten Jobs.

Du kannst supervisorScope mit async oder launch verwenden. Bei async ist besonders wichtig: Exceptions werden beim await() sichtbar. Wenn du ein Deferred nicht sauber auswertest, übersiehst du leicht einen Fehler oder erzeugst unklare Zustände. Bei launch muss eine Exception innerhalb der Kind-Coroutine behandelt werden, wenn sie nicht nur protokolliert oder an einen Handler gehen soll.

Für Lernende ist eine praktische Faustregel hilfreich: Nutze supervisorScope, wenn die Aufgaben fachlich unabhängig sind und du für jede Aufgabe ein eigenes Fehlerergebnis darstellen kannst. Nutze einen normalen coroutineScope, wenn alle Aufgaben gemeinsam erfolgreich sein müssen. Ein Checkout-Prozess, bei dem Warenkorb, Zahlung und Bestellung zusammenpassen müssen, ist meist kein guter Kandidat für Supervisor-Verhalten. Ein Dashboard mit mehreren getrennten Kacheln ist dagegen oft passend.

Auch bei Flow passt dieses Denken. Ein Flow kann Datenströme für die UI liefern, aber einzelne Ladeoperationen im Repository oder Use Case können trotzdem parallel laufen. supervisorScope gehört dann nicht in jeden Flow-Operator, sondern an die Stelle, an der du bewusst mehrere unabhängige Arbeiten koordinierst. So bleibt dein Datenfluss verständlich.

In der Praxis

Stell dir einen Compose-Screen vor, der beim Öffnen drei Bereiche lädt: ein Benutzerprofil, aktuelle Nachrichten und eine optionale Empfehlungsliste. Profil und Nachrichten sind wichtig, Empfehlungen sind nett, aber nicht kritisch. Außerdem sollen Nachrichten nicht verschwinden, nur weil Empfehlungen scheitern. In einem Use Case oder Repository kannst du dafür supervisorScope verwenden.

data class HomeData(
    val profile: Profile?,
    val news: List<NewsItem>,
    val recommendations: List<Recommendation>,
    val errors: List<String>
)

suspend fun loadHomeData(
    profileRepository: ProfileRepository,
    newsRepository: NewsRepository,
    recommendationRepository: RecommendationRepository
): HomeData = supervisorScope {
    val errors = mutableListOf<String>()

    val profileDeferred = async {
        runCatching {
            profileRepository.loadProfile()
        }.onFailure {
            errors += "Profil konnte nicht geladen werden."
        }.getOrNull()
    }

    val newsDeferred = async {
        runCatching {
            newsRepository.loadLatestNews()
        }.onFailure {
            errors += "Nachrichten konnten nicht geladen werden."
        }.getOrDefault(emptyList())
    }

    val recommendationsDeferred = async {
        runCatching {
            recommendationRepository.loadRecommendations()
        }.onFailure {
            errors += "Empfehlungen konnten nicht geladen werden."
        }.getOrDefault(emptyList())
    }

    HomeData(
        profile = profileDeferred.await(),
        news = newsDeferred.await(),
        recommendations = recommendationsDeferred.await(),
        errors = errors
    )
}

Das Beispiel zeigt nicht nur die API, sondern auch die fachliche Entscheidung. Jede Kind-Aufgabe bildet ein eigenes Ergebnis. Wenn Empfehlungen scheitern, bekommst du eine leere Liste und eine Fehlermeldung, aber Nachrichten und Profil dürfen trotzdem erscheinen. In Compose kann dein ViewModel daraus einen UI-State bauen: Profil anzeigen, Nachrichtenliste anzeigen, Hinweis zur fehlenden Empfehlung darstellen.

Eine typische Stolperfalle liegt darin, supervisorScope zu verwenden, aber die Fehler in den Kindern nicht zu behandeln. Dann verhinderst du zwar, dass Geschwister sofort abgebrochen werden, aber du hast noch keinen sinnvollen UI-Zustand. Für Nutzerinnen und Nutzer ist nicht relevant, dass eine Coroutine technisch weitergelaufen ist. Relevant ist, ob die App einen klaren Zustand zeigt: Daten, leere Ansicht, Ladezustand oder Fehlermeldung.

Eine zweite Stolperfalle ist gemeinsame veränderliche Datenstruktur. Im Beispiel wird errors aus mehreren async-Blöcken verändert. Für einfachen Unterrichtscode ist das gut lesbar, in produktivem Code solltest du vorsichtig sein. Besser ist oft, jede Aufgabe ein eigenes Ergebnisobjekt liefern zu lassen und die Fehler danach zusammenzuführen. So vermeidest du Nebenwirkungen und erleichterst Tests.

Zum Beispiel kannst du intern mit einem kleinen Result-Typ arbeiten:

data class LoadResult<T>(
    val value: T,
    val errorMessage: String? = null
)

suspend fun loadRecommendationsSafe(
    repository: RecommendationRepository
): LoadResult<List<Recommendation>> {
    return runCatching {
        repository.loadRecommendations()
    }.fold(
        onSuccess = { LoadResult(it) },
        onFailure = {
            LoadResult(
                value = emptyList(),
                errorMessage = "Empfehlungen konnten nicht geladen werden."
            )
        }
    )
}

Damit wird der Fehler nicht versteckt, sondern als Datenzustand modelliert. Das ist in Android-Architektur besonders nützlich, weil dein ViewModel UI-State aus klaren Werten aufbauen kann. Tests werden ebenfalls einfacher: Du kannst simulieren, dass nur eine Datenquelle fehlschlägt, und prüfen, ob die anderen Daten trotzdem im State landen.

Beim Code-Review solltest du bei supervisorScope immer nach der fachlichen Begründung fragen. Sind die Kind-Aufgaben wirklich unabhängig? Gibt es für jeden Fehler einen sichtbaren oder zumindest protokollierten Umgang? Wird der äußere Scope weiterhin korrekt abgebrochen, wenn der Screen verlassen wird? Wenn diese Fragen nicht beantwortet sind, ist das Supervisor-Verhalten möglicherweise nur ein Pflaster über unsaubere Fehlerbehandlung.

Für Unit-Tests kannst du mit Fake-Repositories arbeiten. Ein Fake liefert erfolgreiche Profildaten, ein zweiter wirft eine Exception bei Empfehlungen, ein dritter liefert Nachrichten. Dein Test sollte dann prüfen, dass die Nachrichten vorhanden bleiben, die Empfehlungsliste leer ist und eine passende Fehlermeldung im State steht. So validierst du nicht nur die Coroutine-Technik, sondern auch die Produktentscheidung hinter partial failure.

Auch Debugging hilft beim Verständnis. Setze Breakpoints in jede Kind-Coroutine und lasse eine gezielt scheitern. Beobachte, ob die anderen async-Blöcke weiterlaufen und ob await() die erwarteten Werte liefert. Danach ersetze supervisorScope testweise durch coroutineScope. Du wirst sehen, wie sich die Fehlerweitergabe verändert. Dieser Vergleich prägt sich gut ein, weil er das abstrakte Konzept sichtbar macht.

Die Entscheidungsregel bleibt kompakt: Wenn ein Fehler alle Ergebnisse fachlich ungültig macht, verwende keinen Supervisor. Wenn ein Fehler nur einen Teilbereich betrifft und die UI den Rest sinnvoll darstellen kann, ist supervisorScope ein passendes Werkzeug. Diese Unterscheidung ist wichtiger als die Syntax.

Fazit

supervisorScope hilft dir, parallele Arbeit in Android-Apps realistischer zu modellieren: Nicht jede fehlgeschlagene Kind-Coroutine muss den gesamten Bildschirm oder Use Case abbrechen. Entscheidend ist, dass du unabhängige Aufgaben wirklich als unabhängige Ergebnisse behandelst und Fehler bewusst in UI-State, Logs oder Tests sichtbar machst. Prüfe dein Verständnis, indem du einen kleinen ViewModel-Test schreibst, in dem eine Datenquelle scheitert und zwei andere erfolgreich bleiben; danach lies den Code im Review mit der Frage, ob jedes Kind einen klaren Fehlerpfad hat.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.