Android Coden
Android 7 min lesen

coroutineScope in Android: strukturierte parallele Arbeit

coroutineScope bündelt parallele Kindaufgaben in Kotlin. Du lernst, wie Fehler sauber weitergegeben werden.

Wenn du in einer Android-App mehrere Dinge gleichzeitig erledigen willst, brauchst du mehr als nur async. Du brauchst eine klare Struktur: Welche Aufgaben gehören zusammen, wann gilt die Arbeit als fertig, und was passiert bei einem Fehler? Genau dafür ist coroutineScope da. Die Funktion hilft dir, parallele Kindaufgaben auszuführen, ohne die Kontrolle über Lebenszyklus, Fehler und Ergebnisfluss zu verlieren.

Was ist das?

coroutineScope ist eine suspend-Funktion aus Kotlin Coroutines. Sie erzeugt einen neuen Coroutine-Bereich innerhalb einer bestehenden suspendierenden Ausführung. In diesem Bereich kannst du Kindaufgaben starten, zum Beispiel mit async oder launch. Der wichtige Punkt: coroutineScope ist erst beendet, wenn alle Kindaufgaben beendet sind. Dadurch bleibt die Arbeit strukturiert.

Das mentale Modell ist eine Klammer um zusammengehörige Arbeit. Alles, was innerhalb dieser Klammer gestartet wird, gehört logisch zu dieser einen Operation. Wenn du eine Profilseite lädst und dafür Benutzerdaten, Statistiken und Einstellungen parallel abrufst, sind diese Abrufe keine unabhängigen Hintergrundjobs. Sie sind Teile derselben Aufgabe: „Profil laden“. coroutineScope bildet genau diese Beziehung im Code ab.

Im Android-Kontext ist das wichtig, weil Apps viele asynchrone Quellen haben: Netzwerk, Datenbank, DataStore, Dateizugriffe oder vorberechnete UI-Daten. Moderne Android-Architektur trennt diese Arbeit häufig in ViewModel, Use Case und Repository. coroutineScope gehört meistens nicht direkt in eine Composable-Funktion, sondern in eine suspendierende Funktion deiner Daten- oder Domain-Schicht. Dort kannst du parallele Arbeit bündeln und dem Aufrufer ein sauberes Ergebnis liefern.

Der Unterschied zu einem frei gewählten Scope wie GlobalScope ist entscheidend. coroutineScope startet keine Arbeit, die irgendwo weiterläuft. Sie bleibt an den aktuellen Aufruf gebunden. Wenn der aufrufende Job abgebrochen wird, werden auch die Kindaufgaben abgebrochen. Wenn eine Kindaufgabe fehlschlägt, wird der Scope ebenfalls fehlschlagen. Das ist strukturierte Nebenläufigkeit: Arbeit hat Eltern, Kinder und klare Grenzen.

Wie funktioniert es?

coroutineScope blockiert keinen Thread. Sie suspendiert nur die aktuelle Coroutine, bis alle Kindaufgaben im Scope abgeschlossen sind. Das passt zu Android, weil der Main Thread frei bleiben muss. Du kannst also in einer suspend-Funktion mehrere Operationen parallel starten und trotzdem eine einfache, lineare API anbieten.

Typisch ist die Kombination mit async, wenn du mehrere Ergebnisse brauchst. async liefert ein Deferred<T>. Mit await() holst du das Ergebnis. Innerhalb von coroutineScope kannst du mehrere async-Aufgaben starten, bevor du auf die Ergebnisse wartest. Dadurch laufen sie parallel, solange die verwendeten Dispatcher und darunterliegenden APIs das erlauben.

Das Fehlerverhalten ist der Kern dieses Themas. Wenn eine Kindaufgabe in coroutineScope eine Exception wirft, wird der gesamte Scope abgebrochen. Andere Kindaufgaben werden ebenfalls gecancelt. Die Exception wird an den Aufrufer weitergegeben. Für viele Android-Fälle ist das genau richtig: Wenn du für einen Bildschirm drei Pflichtdaten lädst und eine Pflichtquelle fehlschlägt, ist das Gesamtergebnis nicht vollständig. Dann sollte der Fehler sichtbar werden, damit das ViewModel einen Fehlerzustand anzeigen kann.

Wichtig ist auch der Unterschied zwischen coroutineScope und supervisorScope. Bei coroutineScope zieht ein Fehler die Geschwisteraufgaben mit. Bei supervisorScope können Geschwister weiterlaufen. Für diesen Artikel bleibt die Entscheidungsgrenze einfach: Nutze coroutineScope, wenn die Kindaufgaben gemeinsam erfolgreich sein müssen. Wenn einzelne Teile optional sind, prüfe bewusst, ob du Fehler lokal behandelst oder eine andere Struktur brauchst.

Im Alltag taucht coroutineScope oft in Funktionen auf, die aus mehreren Quellen ein UI-Modell bauen. Ein Repository kann Daten aus Netzwerk und lokaler Datenbank zusammenführen. Ein Use Case kann mehrere Repositorys parallel fragen. Ein ViewModel ruft diese suspendierende Funktion dann aus viewModelScope auf. Dadurch bleibt der Android-Lebenszyklus außen sauber angebunden, während die innere Arbeit fachlich strukturiert ist.

Du solltest dabei nicht vergessen: coroutineScope wechselt nicht automatisch den Dispatcher. Wenn deine Kindaufgaben I/O-Arbeit ausführen, sollten die aufgerufenen Repository-Funktionen selbst passend mit Dispatchern umgehen oder bereits suspendierende APIs nutzen, die den Thread nicht blockieren. Android-Best-Practices empfehlen, dass suspendierende Funktionen main-safe sind. Das heißt: Der Aufrufer sollte sie vom Main Thread aus starten können, ohne versehentlich blockierende Arbeit dort auszuführen.

In der Praxis

Stell dir vor, du baust eine Profilansicht. Für das Profil brauchst du Nutzerdaten, die letzten Beiträge und lokale Anzeigeeinstellungen. Ohne Parallelisierung würdest du drei Abrufe nacheinander ausführen. Das ist leicht zu lesen, aber unnötig langsam, wenn die Abrufe unabhängig sind. Mit coroutineScope kannst du sie parallel starten und trotzdem ein einzelnes Ergebnis zurückgeben.

data class ProfileScreenData(
    val user: User,
    val posts: List<Post>,
    val settings: DisplaySettings
)

class LoadProfileUseCase(
    private val userRepository: UserRepository,
    private val postRepository: PostRepository,
    private val settingsRepository: SettingsRepository
) {
    suspend operator fun invoke(userId: String): ProfileScreenData = coroutineScope {
        val userDeferred = async {
            userRepository.getUser(userId)
        }

        val postsDeferred = async {
            postRepository.getRecentPosts(userId)
        }

        val settingsDeferred = async {
            settingsRepository.getDisplaySettings()
        }

        ProfileScreenData(
            user = userDeferred.await(),
            posts = postsDeferred.await(),
            settings = settingsDeferred.await()
        )
    }
}

Der Code sagt: Diese drei Aufgaben gehören zu einer Operation. Erst wenn alle Ergebnisse vorhanden sind, wird ProfileScreenData gebaut. Wenn getRecentPosts() eine Exception wirft, wird auch das Laden der anderen Kindaufgaben abgebrochen, sofern sie noch laufen. Der Aufrufer bekommt den Fehler. Ein ViewModel kann ihn dann in einen UI-State übersetzen.

class ProfileViewModel(
    private val loadProfile: LoadProfileUseCase
) : ViewModel() {

    private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val state: StateFlow<ProfileState> = _state

    fun load(userId: String) {
        viewModelScope.launch {
            _state.value = ProfileState.Loading

            _state.value = try {
                ProfileState.Content(loadProfile(userId))
            } catch (exception: IOException) {
                ProfileState.Error("Profil konnte nicht geladen werden.")
            }
        }
    }
}

Beachte die Rollenverteilung. viewModelScope verbindet die Arbeit mit dem ViewModel-Lebenszyklus. coroutineScope strukturiert die innere parallele Arbeit. Das ViewModel muss nicht wissen, dass drei Abrufe parallel laufen. Es sieht nur eine suspendierende Operation, die entweder Daten liefert oder fehlschlägt.

Eine typische Stolperfalle ist, async außerhalb eines klaren Scopes zu starten und die Deferred-Objekte später irgendwo aufzubewahren. Dadurch wird schwer nachvollziehbar, wer die Arbeit besitzt, wann sie beendet sein muss und wer Fehler behandelt. Gerade in Android führt das schnell zu versteckten Bugs: ein Bildschirm ist geschlossen, aber Arbeit läuft weiter; ein Fehler wird zu spät sichtbar; oder ein Test wartet nicht auf alle gestarteten Aufgaben.

Eine zweite Stolperfalle ist falsche Fehlererwartung. Manche Lernende erwarten, dass bei drei parallelen Abrufen zwei erfolgreiche Ergebnisse erhalten bleiben, wenn der dritte Abruf fehlschlägt. Das ist bei coroutineScope nicht das Standardmodell. Der Scope steht für gemeinsame Arbeit. Scheitert ein Pflichtteil, scheitert die gesamte Operation. Wenn du Teilergebnisse bewusst erlaubst, musst du das im Code ausdrücken, zum Beispiel durch lokale try/catch-Blöcke pro optionaler Aufgabe und ein Ergebnisobjekt, das fehlende Teile modelliert. Diese Entscheidung sollte im Code-Review erkennbar sein.

Eine einfache Entscheidungsregel hilft: Verwende coroutineScope, wenn du innerhalb einer suspendierenden Funktion mehrere voneinander unabhängige Pflichtaufgaben parallel ausführen willst und das Ergebnis nur gültig ist, wenn alle erfolgreich sind. Verwende keine neue, langlebige Scope-Instanz in einem Repository, nur um parallele Arbeit zu starten. Langlebige Scopes gehören an klare Lebenszyklusgrenzen, etwa viewModelScope im ViewModel oder ein explizit injizierter Application-Scope für Arbeit, die wirklich länger leben darf.

Beim Testen kannst du dein Verständnis gut prüfen. Schreibe einen Test, in dem eine der Repository-Funktionen absichtlich fehlschlägt. Erwarte, dass der Use Case eine Exception weitergibt und kein teilweise gefülltes ProfileScreenData liefert. Prüfe außerdem mit Debug-Logs oder Breakpoints, dass die Abrufe parallel starten. In Code-Reviews solltest du auf drei Fragen achten: Gehören die Kindaufgaben wirklich zusammen? Ist das Fehlerverhalten fachlich korrekt? Ist der Scope an den passenden Lebenszyklus gebunden?

Auch mit Flow bleibt die Idee relevant. Ein Flow kann Werte über Zeit liefern, während coroutineScope eine konkrete suspendierende Arbeit strukturiert. Wenn du innerhalb eines Flow-Builders parallele Einmalabrufe machst, gelten dieselben Fragen zu Kindaufgaben und Fehlern. Du solltest aber nicht jedes Flow-Problem mit coroutineScope lösen. Bleibe bei der konkreten Aufgabe: parallele Kindarbeit innerhalb einer suspendierenden Operation, mit gemeinsamem Erfolg oder gemeinsamem Fehler.

Fazit

coroutineScope ist ein kleines, aber wichtiges Werkzeug für saubere Android-Coroutines. Du nutzt es, wenn mehrere Kindaufgaben parallel laufen sollen, aber als eine strukturierte Arbeitseinheit gelten. Baue dir eine kleine Use-Case-Funktion mit zwei oder drei parallelen Repository-Aufrufen, setze Breakpoints in die Kindaufgaben und simuliere einen Fehler in einer davon. Wenn du danach genau erklären kannst, welche Aufgabe abgebrochen wird, welche Exception beim Aufrufer landet und warum der Android-Lebenszyklus trotzdem sauber bleibt, hast du das Konzept praktisch verstanden.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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