Android Coden
Android 7 min lesen

Async Capstone: Flow, WorkManager und Abbruch

Du baust eine Funktion mit Live-Daten und stabilem Refresh. Dabei lernst du Flow, WorkManager und Abbruch sauber zu verbinden.

Ein Async Capstone ist eine kleine, aber vollständige Übungsfunktion, in der du mehrere asynchrone Bausteine zusammenführst: Live-Daten mit Flow, zuverlässigen Refresh mit WorkManager und sauberen Abbruch mit Coroutines. Das Ziel ist nicht, möglichst viele APIs zu zeigen, sondern eine realistische Denkweise zu trainieren: Deine App zeigt Daten aus einer stabilen Quelle, aktualisiert sie im Hintergrund und bleibt trotzdem kontrollierbar, testbar und ressourcenschonend.

Was ist das?

Ein Capstone ist eine Abschlussaufgabe für ein Themengebiet. Beim Async Capstone baust du eine Funktion, die sich wie ein echtes Android-Feature verhält. Stell dir eine Liste gespeicherter Artikel, Aufgaben, Wetterdaten oder Nachrichten vor. Die UI soll sofort etwas anzeigen, neue Daten automatisch übernehmen und auch dann aktualisiert werden können, wenn die App gerade nicht aktiv genutzt wird.

Die drei Kernbegriffe haben dabei klare Rollen. Flow beschreibt einen Datenstrom. Statt einmal Daten zu laden und danach zu hoffen, dass sie aktuell bleiben, beobachtest du Änderungen. WorkManager übernimmt Arbeit, die zuverlässig im Hintergrund laufen soll, zum Beispiel ein regelmäßiger oder einmaliger Refresh. Cancellation sorgt dafür, dass laufende Arbeit beendet werden kann, wenn sie nicht mehr gebraucht wird. Das betrifft etwa Netzwerkaufrufe, Mapper, Datenbankoperationen und lange Ketten aus Flow-Operatoren.

Im modernen Android-Kontext passt dieses Thema besonders gut zur typischen Architektur mit UI, ViewModel, Repository, lokaler Datenquelle und Remote-Datenquelle. Jetpack Compose sammelt UI-Zustände aus dem ViewModel. Das ViewModel fragt keine Details der Datenbank oder des Netzwerks ab, sondern spricht mit einem Repository. Das Repository entscheidet, welche Quelle gelesen wird und wann aktualisiert wird. WorkManager stößt den Refresh an, aber die UI liest weiterhin aus derselben beobachtbaren Quelle.

Das ist wichtig, weil viele Apps nicht an einem einzelnen suspend fun load() scheitern, sondern an den Übergängen: Was sieht der Nutzer beim Start? Was passiert ohne Internet? Wer startet den Refresh? Wird doppelt geladen? Wird ein Job abgebrochen, wenn der Bildschirm verschwindet? Ein Async Capstone zwingt dich, diese Fragen nicht theoretisch, sondern im Code zu beantworten.

Wie funktioniert es?

Das mentale Modell ist: Die UI beobachtet lokale, stabile Daten. Der Hintergrund aktualisiert diese Daten. Der Flow verbindet beides. Du solltest also nicht in der UI regelmäßig das Netzwerk aufrufen. Ebenso sollte dein Worker nicht versuchen, direkt Compose-State zu verändern. Beide Seiten treffen sich in der Datenebene, meistens über Datenbank und Repository.

Ein typischer Ablauf sieht so aus: Die lokale Datenbank liefert einen Flow<List<Item>>. Das Repository gibt daraus einen UI-nahen Stream zurück, zum Beispiel Flow<List<Article>> oder Flow<UiState>. Das ViewModel wandelt diesen Stream mit stateIn in einen Zustand um, den Compose beobachten kann. Ein Worker ruft später eine Repository-Methode wie refresh() auf. Diese Methode lädt Daten aus dem Netzwerk, speichert sie in der lokalen Datenbank und beendet sich. Sobald die Datenbank aktualisiert wurde, emittiert der Flow automatisch neue Werte. Die UI muss nichts über den Worker wissen.

Coroutines liefern die Ausführungseinheiten. Eine suspend-Funktion kann warten, ohne einen Thread zu blockieren. Flow ist darauf aufgebaut und arbeitet kalt, solange er nicht gesammelt wird. Das bedeutet: Ein Flow beschreibt eine Berechnung oder Beobachtung, aber er läuft erst, wenn jemand ihn sammelt. Im ViewModel passiert das häufig über stateIn(viewModelScope, ...). In Compose wird daraus etwa per collectAsStateWithLifecycle() ein beobachtbarer Zustand. So wird die Sammlung an den Lebenszyklus gebunden und läuft nicht unnötig weiter, wenn der Screen nicht aktiv ist.

WorkManager ist für Arbeit gedacht, die garantiert geplant und unter passenden Bedingungen ausgeführt werden soll. Du verwendest ihn für Aufgaben wie Synchronisierung, Uploads oder regelmäßige Aktualisierung. Für Arbeit, die nur während eines geöffneten Screens relevant ist, ist er dagegen nicht die erste Wahl. Dort reichen ViewModel-Coroutines meist aus. Eine gute Entscheidungsregel lautet: Wenn das Ergebnis auch dann wichtig ist, wenn die App geschlossen wurde oder das Gerät später wieder Netz hat, prüfe WorkManager. Wenn die Arbeit nur den aktuellen UI-Vorgang betrifft, nutze eine Coroutine im passenden Scope.

Cancellation ist kein Sonderfall, sondern ein normaler Teil des Kontrollflusses. Wenn ein Nutzer den Screen verlässt, sollte die UI-Sammlung enden. Wenn ein neuer Suchbegriff eingegeben wird, darf der alte Request abgebrochen werden. Wenn ein Worker gestoppt wird, sollte deine Arbeit kooperativ abbrechen. Das funktioniert nur, wenn du keine blockierenden Endlosschleifen baust, keine Exceptions pauschal verschluckst und CancellationException nicht aus Versehen als normalen Fehler behandelst.

Im Alltag begegnet dir dieses Muster überall: Feed aktualisieren, Warenkorb synchronisieren, Favoriten offline bereitstellen, Profil im Hintergrund nachladen oder App-Daten nach einem Login neu aufbauen. Der saubere Aufbau ist immer ähnlich. Die UI zeigt den Zustand. Das Repository kapselt Datenregeln. Flow transportiert Änderungen. WorkManager plant robuste Arbeit. Cancellation hält das System beweglich.

In der Praxis

Nimm als Beispiel eine Artikelliste. Die App soll beim Öffnen sofort lokal gespeicherte Artikel anzeigen. Zusätzlich soll sie regelmäßig im Hintergrund aktualisieren. Der Worker schreibt neue Daten in die Datenbank. Die UI muss nicht wissen, ob die Daten gerade aus dem Cache oder vom letzten Netz-Refresh kommen.

Ein kompaktes Repository könnte so aussehen:

class ArticleRepository(
    private val dao: ArticleDao,
    private val api: ArticleApi,
    private val ioDispatcher: CoroutineDispatcher
) {
    val articles: Flow<List<Article>> =
        dao.observeArticles()
            .map { entities -> entities.map { it.toDomain() } }
            .flowOn(ioDispatcher)

    suspend fun refresh() = withContext(ioDispatcher) {
        val remoteArticles = api.fetchArticles()
        dao.replaceAll(remoteArticles.map { it.toEntity() })
    }
}

Das ViewModel macht daraus einen stabilen UI-State:

class ArticleViewModel(
    repository: ArticleRepository
) : ViewModel() {
    val uiState: StateFlow<ArticleUiState> =
        repository.articles
            .map { articles -> ArticleUiState.Content(articles) }
            .catch { error -> emit(ArticleUiState.Error(error.message ?: "Unbekannter Fehler")) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = ArticleUiState.Loading
            )
}

Der Worker führt nur den Refresh aus:

class ArticleRefreshWorker(
    appContext: Context,
    params: WorkerParameters,
    private val repository: ArticleRepository
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        return try {
            repository.refresh()
            Result.success()
        } catch (e: IOException) {
            Result.retry()
        } catch (e: CancellationException) {
            throw e
        } catch (e: Exception) {
            Result.failure()
        }
    }
}

An diesem Beispiel sind mehrere Details wichtig. Erstens liest die UI nicht direkt vom Netzwerk. Sie beobachtet den Repository-Flow. Zweitens schreibt der Worker nur in die Datenquelle, aus der die UI ohnehin liest. Dadurch entsteht kein zweiter, konkurrierender Zustand. Drittens wird CancellationException wieder geworfen. Das ist eine typische Stolperfalle: Wer in einem breiten catch (e: Exception) alle Fehler gleich behandelt, kann den Abbruch einer Coroutine unabsichtlich blockieren oder falsch als fachlichen Fehler melden.

Eine weitere Stolperfalle ist doppelter Refresh. Wenn du beim Öffnen des Screens, beim Pull-to-refresh und zusätzlich periodisch per WorkManager denselben Netzaufruf startest, bekommst du unnötige Last und schwer erklärbare Zustände. Plane deshalb bewusst: Der Screen darf einen manuellen Refresh auslösen, WorkManager kümmert sich um Hintergrundaktualisierung, und das Repository sollte Wiederholungen oder parallele Schreibvorgänge kontrollieren. Je nach App reicht dafür ein Mutex, eine Datenbanktransaktion oder eine einfache Regel wie: Ein automatischer Refresh läuft nur, wenn der letzte erfolgreiche Refresh alt genug ist.

Auch Fehlerzustände brauchen eine klare Grenze. Ein fehlgeschlagener Hintergrundrefresh sollte nicht automatisch die gesamte Liste aus der UI entfernen. Wenn lokale Daten vorhanden sind, zeigst du sie weiter und dokumentierst den Fehler optional als Status. Das passt zur Offline-first-Denkweise: Die lokale Datenquelle ist nicht nur ein Cache-Anhängsel, sondern die verlässliche Quelle für die Anzeige. Das Netzwerk aktualisiert sie, wenn es möglich ist.

Beim Testen kannst du dieses Muster gut prüfen. Für das Repository testest du, ob refresh() die erwarteten Daten in die lokale Quelle schreibt. Für den Flow testest du, ob nach einer Änderung neue Werte emittiert werden. Für das ViewModel prüfst du den Übergang von Loading zu Content oder Error. Beim Worker testest du, ob Netzwerkfehler zu Result.retry() führen und nicht zu einem stillen Erfolg. In einem Code-Review solltest du besonders auf Scope-Wahl, Abbruchverhalten, breite catch-Blöcke, doppelte Zustände und direkte Netzwerkzugriffe aus der UI achten.

Zum Debuggen hilft ein einfacher Ablauf: Starte die App mit leerer Datenbank, beobachte den initialen Zustand, löse einen Refresh aus, schalte das Netzwerk aus und prüfe, ob vorhandene Daten sichtbar bleiben. Danach beendest du die App und lässt den Worker laufen. Wenn die Datenbank aktualisiert wurde, sollte der nächste App-Start sofort den neuen Stand zeigen. Genau dieser Ablauf zeigt, ob Flow, WorkManager und Cancellation wirklich zusammenarbeiten oder nur einzeln funktionieren.

Fazit

Der Async Capstone zeigt dir, ob du asynchrone Android-Entwicklung als System verstehst: Flow liefert fortlaufende Zustände, WorkManager übernimmt zuverlässige Hintergrundarbeit, und Cancellation hält laufende Arbeit kontrollierbar. Baue eine kleine Funktion mit lokaler Datenquelle, Repository, UI-State und Worker, und prüfe sie bewusst mit Tests, Debugger und Code-Review. Wenn du erklären kannst, wer Daten liest, wer sie schreibt, wer Arbeit startet und wie Abbruch behandelt wird, hast du den Kern dieses Roadmap-Schritts verstanden.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

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