Android Coden
Android 8 min lesen

Suspend Functions in Kotlin

Suspend Functions machen asynchrone APIs in Kotlin lesbar. Du lernst, wie Arbeit pausiert, ohne den Main Thread zu blockieren.

Suspend Functions sind ein Kernwerkzeug moderner Android-Entwicklung mit Kotlin. Sie helfen dir, APIs für Datenbankzugriffe, Netzwerkaufrufe oder Dateioperationen so zu bauen, dass der Main Thread frei bleibt und dein Code trotzdem wie normaler, schrittweiser Kotlin-Code lesbar ist.

Was ist das?

Eine Suspend Function ist eine Kotlin-Funktion, die mit dem Schlüsselwort suspend markiert ist. Sie darf ihre Ausführung an bestimmten Punkten pausieren und später fortsetzen. Wichtig ist dabei: Pausieren bedeutet nicht, dass ein Thread blockiert wird. Die Coroutine, in der die Funktion läuft, gibt den Thread frei, während sie auf ein Ergebnis wartet. Wenn das Ergebnis verfügbar ist, wird die Ausführung an der passenden Stelle fortgesetzt.

Das mentale Modell ist: Eine normale Funktion läuft durch, bis sie fertig ist oder einen Fehler wirft. Eine Suspend Function kann zwischendurch sagen: “Hier muss ich warten, aber der Thread kann in der Zwischenzeit andere Arbeit erledigen.” Genau dieses Verhalten brauchst du in Android, weil der Main Thread für UI, Eingaben, Animationen und Compose-Recomposition zuständig ist. Wenn du ihn mit Netzwerk oder Datenbank blockierst, reagiert die App träge oder Android meldet sogar einen Fehler wegen nicht reagierender Oberfläche.

Im Android-Kontext sind Suspend Functions besonders wichtig für APIs in der Daten- und Domänenschicht. Ein Repository kann zum Beispiel eine Funktion suspend fun loadUser(id: String): User anbieten. Der Aufrufer muss nicht wissen, ob die Daten aus Retrofit, Room, DataStore oder einem Cache kommen. Die API sagt nur: “Diese Operation kann warten müssen, rufe sie aus einem Coroutine-Kontext auf.” Damit bleibt die Schnittstelle klar und der Aufruf im ViewModel gut lesbar.

Der Begriff suspend ist dabei bewusst enger als “asynchron”. Eine Suspend Function ist keine eigene Hintergrundaufgabe und startet nicht automatisch parallel. Sie beschreibt nur, dass die Funktion innerhalb einer Coroutine unterbrechbar ist. Erst ein Coroutine Builder wie viewModelScope.launch oder async startet eine Coroutine. Diese Trennung ist wichtig, weil sie deine APIs kontrollierbar macht: Die aufrufende Schicht entscheidet über Lebenszyklus, Abbruch und Fehlerbehandlung.

Wie funktioniert es?

Technisch übersetzt der Kotlin-Compiler Suspend Functions in Code, der den aktuellen Zustand der Ausführung speichern und später wieder aufnehmen kann. Du musst diese Details nicht jedes Mal im Kopf behalten, aber sie erklären, warum suspend nur aus bestimmten Kontexten aufrufbar ist. Eine Suspend Function kann aus einer anderen Suspend Function heraus aufgerufen werden oder aus einer Coroutine. Aus einer normalen Funktion geht das nicht direkt, weil dort kein Mechanismus vorhanden ist, um die Ausführung sauber zu pausieren und fortzusetzen.

In Android nutzt du dafür meistens Scopes, die an einen Lebenszyklus gebunden sind. Im ViewModel ist viewModelScope der Standard. Wenn das ViewModel beendet wird, werden laufende Coroutines in diesem Scope abgebrochen. Dadurch passen Suspend Functions gut zu Architektur-Komponenten: Das ViewModel startet die Arbeit, das Repository stellt suspend APIs bereit, und die Datenquelle führt konkrete Operationen aus.

Ein häufiger Punkt der Verwirrung betrifft Threads. suspend allein verschiebt Arbeit nicht auf einen Hintergrundthread. Wenn eine Suspend Function intern einen bereits suspendierbaren API-Aufruf nutzt, etwa eine Retrofit-Schnittstelle mit suspend, ist das in der Regel passend. Wenn du aber blockierenden Code ausführst, zum Beispiel eine alte Datei-API oder einen synchronen Netzwerkclient, blockiert dieser Code weiterhin den aktuellen Thread. Dann musst du mit withContext(Dispatchers.IO) bewusst auf einen geeigneten Dispatcher wechseln.

Suspend Functions passen außerdem gut zu klaren API-Grenzen. Eine Funktion, die genau ein Ergebnis liefert oder fehlschlägt, ist oft ein guter Kandidat für suspend. Beispiele sind refreshProfile(), saveSettings(settings) oder fetchArticle(id). Wenn du dagegen fortlaufende Werte beobachtest, etwa Änderungen in einer Datenbank oder einen UI-Zustand, ist Flow oft passender. Eine Suspend Function steht also eher für eine einzelne Operation, ein Flow eher für einen Strom von Werten.

Fehler werden in Suspend Functions wie normale Kotlin-Exceptions behandelt. Wenn ein Netzwerkaufruf fehlschlägt, kann die Suspend Function eine Exception werfen. In professionellem Android-Code solltest du aber bewusst entscheiden, ob deine API Exceptions nach außen gibt oder ein Ergebnisobjekt wie Result, eine eigene sealed class oder ein Domänenmodell mit Fehlerzustand nutzt. Wichtig ist, dass die Entscheidung konsistent bleibt. Eine suspend API, die manchmal Exceptions wirft und manchmal Fehlerwerte zurückgibt, ist schwer zu testen und schwer zu lesen.

Auch Cancellation gehört zum Konzept. Coroutines können abgebrochen werden, etwa wenn der Nutzer den Screen verlässt. Suspendierbare APIs reagieren darauf in der Regel kooperativ. Wenn du in einer Suspend Function lange Schleifen oder blockierenden Code schreibst, solltest du prüfen, ob Abbruch noch möglich ist. Für Android ist das mehr als Stilfrage: Nicht abgebrochene Arbeit kann Akku, Datenvolumen und Speicher belasten.

In der Praxis

In der täglichen Android-Entwicklung siehst du Suspend Functions oft zuerst in Repositories. Das ViewModel ruft eine suspend API auf, aktualisiert den UI-State und Compose rendert den aktuellen Zustand. So bleibt die UI-Schicht frei von Details über Netzwerk, Datenbank und Cache. Gleichzeitig kann das Repository später intern erweitert werden, etwa um Offline-first-Logik: erst lokale Daten lesen, dann remote aktualisieren, anschließend speichern. Die öffentliche API kann dabei stabil bleiben.

Ein typisches Beispiel sieht so aus:

data class Article(
    val id: String,
    val title: String,
    val body: String
)

interface ArticleRemoteDataSource {
    suspend fun fetchArticle(id: String): Article
}

interface ArticleLocalDataSource {
    suspend fun getArticle(id: String): Article?
    suspend fun saveArticle(article: Article)
}

class ArticleRepository(
    private val remote: ArticleRemoteDataSource,
    private val local: ArticleLocalDataSource
) {
    suspend fun getArticle(id: String): Article {
        val cached = local.getArticle(id)
        if (cached != null) return cached

        val fresh = remote.fetchArticle(id)
        local.saveArticle(fresh)
        return fresh
    }
}

data class ArticleUiState(
    val loading: Boolean = false,
    val article: Article? = null,
    val errorMessage: String? = null
)

class ArticleViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    private val _state = MutableStateFlow(ArticleUiState())
    val state: StateFlow<ArticleUiState> = _state.asStateFlow()

    fun loadArticle(id: String) {
        viewModelScope.launch {
            _state.value = ArticleUiState(loading = true)

            runCatching {
                repository.getArticle(id)
            }.onSuccess { article ->
                _state.value = ArticleUiState(article = article)
            }.onFailure {
                _state.value = ArticleUiState(
                    errorMessage = "Artikel konnte nicht geladen werden."
                )
            }
        }
    }
}

An diesem Beispiel siehst du mehrere wichtige Grenzen. getArticle ist suspend, weil die Funktion auf lokale oder entfernte Daten warten kann. Sie startet aber selbst keine Coroutine. Das ViewModel entscheidet mit viewModelScope.launch, wann die Operation beginnt und an welchen Lebenszyklus sie gebunden ist. Das Repository bleibt dadurch testbar und unabhängig von der UI.

Eine gute Entscheidungsregel lautet: Markiere eine API als suspend, wenn sie eine einzelne potenziell wartende Operation beschreibt und der Aufrufer das Ergebnis erst nach Abschluss sinnvoll verwenden kann. Starte in Repositories und Datenquellen nur dann eigene Coroutines, wenn du wirklich Nebenarbeit entkoppeln musst und deren Lebenszyklus bewusst geregelt ist. Für die meisten Lern- und Junior-Szenarien ist es sauberer, suspend APIs nach oben anzubieten und den Startpunkt im ViewModel oder in einem klar definierten Use Case zu halten.

Eine typische Stolperfalle ist die Annahme, suspend mache jede Arbeit automatisch non-blocking. Das stimmt nicht. Wenn du innerhalb einer Suspend Function Thread.sleep(2000) aufrufst, blockierst du trotzdem den Thread. In Coroutine-Code nutzt du stattdessen delay(2000), weil delay suspendiert. Ähnlich ist es bei alten synchronen APIs: Wenn sie blockieren, kapselst du sie mit withContext(Dispatchers.IO), damit sie nicht auf dem Main Thread laufen.

Ein zweiter häufiger Fehler ist GlobalScope. Für Android-Apps ist GlobalScope.launch fast nie die richtige Wahl, weil die Arbeit dann nicht sinnvoll an einen Screen, ein ViewModel oder einen App-weiten Verantwortungsbereich gebunden ist. Wenn der Nutzer navigiert, kann die Arbeit weiterlaufen, obwohl ihr Ergebnis niemand mehr braucht. Nutze stattdessen passende Scopes wie viewModelScope oder injizierte Scopes für App-weite Aufgaben, wenn du deren Lebenszyklus wirklich definieren kannst.

Auch beim Testen profitierst du von Suspend Functions. Du kannst Repository-Methoden direkt in Coroutine-Tests aufrufen und Fake-Datenquellen verwenden. Dadurch prüfst du nicht nur, ob ein Ergebnis zurückkommt, sondern auch, ob die API-Grenze sinnvoll ist. Ein Test könnte etwa sicherstellen, dass zuerst der Cache verwendet wird und der Remote-Aufruf nur erfolgt, wenn keine lokalen Daten vorhanden sind. In Code-Reviews solltest du gezielt nach drei Fragen suchen: Blockiert die Suspend Function irgendwo heimlich? Ist der Coroutine-Startpunkt an den richtigen Lebenszyklus gebunden? Ist die Fehlerbehandlung für Aufrufer eindeutig?

Für Compose ist die Trennung ebenfalls hilfreich. Composables sollten nicht direkt beliebige suspend APIs in normalen Funktionskörpern aufrufen, weil Composables mehrfach ausgeführt werden können. Stattdessen kommt der Zustand aus dem ViewModel, oder du nutzt Compose-seitige Effekte wie LaunchedEffect, wenn eine UI-nahe suspend Operation an einen bestimmten Schlüssel gebunden ist. Auch hier gilt: Die suspend API beschreibt die wartende Arbeit, der passende Scope bestimmt, wann sie läuft.

Fazit

Suspend Functions geben dir eine klare Sprache für wartende Arbeit in Kotlin-Android-Apps: Eine API darf pausieren, ohne den Main Thread zu blockieren, und bleibt dabei lesbar wie normaler Kotlin-Code. Prüfe dein Verständnis aktiv an einem kleinen Repository: Baue eine suspend fun, rufe sie aus viewModelScope.launch auf, simuliere Erfolg und Fehler, setze einen Breakpoint vor und nach einem suspendierenden Aufruf, und kontrolliere im Code-Review, ob kein blockierender Call im falschen Kontext steckt. So lernst du nicht nur die Syntax, sondern auch die Architekturentscheidung dahinter: Suspend Functions sind API-Verträge für einzelne asynchrone Operationen, deren Lebenszyklus der Aufrufer sauber steuern muss.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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