Android Coden
Android 8 min lesen

Work Retries: Wiederholungen für robuste Hintergrundarbeit

Lerne, wie du temporäre Fehler in Hintergrundarbeit gezielt wiederholst und dabei Akku, Datenvolumen und Nutzervertrauen schützt.

Work Retries sind ein kleines, aber wichtiges Werkzeug für robuste Android-Apps: Wenn Hintergrundarbeit an einem vorübergehenden Problem scheitert, startest du sie nicht sofort beliebig oft neu, sondern lässt sie kontrolliert erneut laufen. So kann deine App Netzwerkprobleme, Serverüberlastung oder kurzzeitig fehlende Voraussetzungen abfedern, ohne Akku, Datenvolumen oder Geduld deiner Nutzer unnötig zu belasten.

Was ist das?

Work Retries bedeuten, dass eine fehlgeschlagene Hintergrundaufgabe später erneut ausgeführt wird. Im Android-Kontext geht es dabei meist um Arbeit, die nicht direkt an einen sichtbaren Screen gebunden ist: Daten synchronisieren, Uploads nachholen, Log-Dateien senden, lokale Änderungen mit einem Backend abgleichen oder Medien verarbeiten. Solche Aufgaben laufen häufig über Jetpack WorkManager, während die eigentliche Arbeit in Kotlin oft mit Coroutines umgesetzt wird.

Das zentrale Problem ist nicht der Fehler selbst, sondern seine Art. Manche Fehler sind dauerhaft: Eine Datei existiert nicht, ein Request ist fachlich ungültig, ein Nutzer hat keine Berechtigung oder ein Datensatz verletzt eine Regel des Backends. Ein Retry würde daran nichts ändern. Andere Fehler sind temporär: Das Gerät ist gerade offline, der Server antwortet mit einem Timeout, ein Token wird aktualisiert, oder die Verbindung bricht beim Upload ab. Für diese Fälle ist ein Retry sinnvoll.

Dein mentales Modell sollte so aussehen: Ein Retry ist kein Ersatz für Fehlerbehandlung, sondern eine geplante Antwort auf einen Fehler, der sich mit hoher Wahrscheinlichkeit von selbst beheben kann. Du sagst der App damit: „Diese Arbeit ist weiterhin gültig, aber der Zeitpunkt war schlecht.“ Genau hier kommen die Begriffe Result.retry, Backoff und Resilience zusammen.

Result.retry beschreibt in WorkManager die Entscheidung, dass ein Worker nicht endgültig gescheitert ist. Backoff beschreibt die Wartezeit bis zum nächsten Versuch. Resilience meint die Fähigkeit deiner App, unter realen Bedingungen stabil zu bleiben: schlechte Netze, wechselnde App-Zustände, begrenzter Akku, Prozessende, Neustarts und Serverprobleme.

In modernen Android-Apps passt dieses Thema direkt zu Kotlin, Coroutines und sauberer Architektur. Coroutines helfen dir, asynchrone Arbeit lesbar zu schreiben. Repository- und Use-Case-Schichten kapseln fachliche Operationen. WorkManager plant die Ausführung zuverlässig im Hintergrund. Work Retries verbinden diese Bausteine mit einer klaren Regel: Wiederhole nur Arbeit, die weiterhin gültig ist und bei der ein späterer Versuch eine echte Chance hat.

Wie funktioniert es?

Ein Worker liefert am Ende ein Ergebnis zurück. Vereinfacht gibt es drei relevante Zustände: Erfolg, endgültiger Fehler und erneuter Versuch. Erfolg bedeutet, dass die Arbeit abgeschlossen ist. Ein endgültiger Fehler bedeutet, dass weitere Versuche nicht helfen. Ein erneuter Versuch bedeutet, dass die Arbeit später nochmal geplant werden soll.

Der wichtige Punkt ist: Die Retry-Entscheidung gehört möglichst nah an die Stelle, an der du den Fehler fachlich bewerten kannst. Ein IOException bei einem Upload ist oft temporär. Eine 400 Bad Request-Antwort ist meist dauerhaft, weil die gesendeten Daten falsch sind. Eine 401 Unauthorized-Antwort kann temporär sein, wenn deine App ein Token erneuern kann; sie kann aber dauerhaft sein, wenn der Nutzer abgemeldet wurde. Du brauchst also keine pauschale Regel „bei Exception immer retry“. Du brauchst eine bewusste Fehlerklassifikation.

Backoff verhindert, dass deine App aggressiv wiederholt. Wenn du bei jedem Fehler sofort erneut startest, erzeugst du Last auf Gerät und Server. Außerdem verschlechterst du die Situation bei Netzproblemen: Mehr Versuche bedeuten nicht automatisch mehr Erfolg. Ein Backoff setzt zwischen den Versuchen eine Wartezeit. Diese Wartezeit kann konstant sein oder mit jedem Versuch wachsen. In der Praxis ist ein wachsender Abstand oft sinnvoll, weil viele Störungen nach kurzer Zeit nicht verschwinden, aber nach einigen Minuten oder Stunden behoben sein können.

Coroutines spielen dabei eine andere Rolle als WorkManager. Eine Coroutine macht asynchrone Arbeit innerhalb eines laufenden Ausführungskontexts handhabbar. WorkManager entscheidet, wann die Arbeit laufen darf und was nach einem Ergebnis passiert. Du solltest nicht versuchen, dauerhafte Hintergrundarbeit nur mit einer Coroutine im ViewModel zu lösen. Ein ViewModel lebt nur, solange der zugehörige UI-Bereich relevant ist. Für Arbeit, die Prozessende oder App-Neustarts überstehen soll, brauchst du eine geplante Hintergrundlösung.

Flow kann in diesem Umfeld helfen, Zustände sichtbar zu machen. Du kannst zum Beispiel den Status einer Synchronisation aus einer Datenbank als Flow in Compose anzeigen. Der Retry selbst sollte aber nicht aus der UI heraus in einer Schleife ausgelöst werden. Die UI zeigt Status, Fortschritt oder Fehlermeldungen. Die Hintergrundkomponente entscheidet über geplante Wiederholung.

Im Alltag taucht dieses Thema häufig bei Synchronisation auf. Eine Nutzerin erstellt offline einen Eintrag. Deine App speichert ihn lokal und plant einen Upload. Wenn das Gerät später Netz hat, startet der Worker. Scheitert der Upload wegen eines Timeouts, gibst du Result.retry zurück. Scheitert er, weil der Eintrag serverseitig ungültig ist, markierst du ihn lokal als problematisch und gibst einen endgültigen Fehler oder Erfolg mit Fehlerstatus in deiner Domäne zurück. Das klingt ungewohnt, ist aber wichtig: Nicht jeder fachliche Fehler ist ein technischer Worker-Fehler.

Eine gute Architektur trennt daher technische Ausführung und fachlichen Zustand. Der Worker ruft einen Use Case auf, etwa syncPendingNotes(). Dieser Use Case spricht mit Repository, Netzwerk und Datenbank. Er kann ein Ergebnis liefern, das unterscheidet: abgeschlossen, temporär fehlgeschlagen, dauerhaft fehlgeschlagen. Der Worker übersetzt dieses Ergebnis dann in Result.success, Result.retry oder Result.failure.

Du solltest außerdem die Anzahl der Versuche im Blick behalten. WorkManager verfolgt, wie oft ein Worker bereits versucht wurde. Diese Information ist wertvoll. Nach vielen Versuchen ist ein Problem vielleicht nicht mehr temporär. Dann kann es besser sein, den Datensatz als „Synchronisation fehlgeschlagen“ zu markieren, Telemetrie zu erfassen oder dem Nutzer eine konkrete Aktion anzubieten. Resilience bedeutet nicht, endlos zu wiederholen. Resilience bedeutet, kontrolliert weiterzuarbeiten und klare Zustände zu erzeugen.

In der Praxis

Stell dir vor, deine App lädt ausstehende Notizen in ein Backend hoch. Die Notizen sind lokal gespeichert. Der Worker soll nur dann wiederholen, wenn ein temporärer Fehler auftritt. Fachliche Fehler werden lokal markiert, damit die UI sie anzeigen kann.

class SyncNotesWorker(
    appContext: Context,
    params: WorkerParameters,
    private val syncNotes: SyncPendingNotesUseCase
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        return when (val outcome = syncNotes()) {
            SyncOutcome.Success -> {
                Result.success()
            }

            is SyncOutcome.TemporaryFailure -> {
                if (runAttemptCount < 5) {
                    Result.retry()
                } else {
                    syncNotes.markAsWaitingForUserAction(outcome.reason)
                    Result.failure()
                }
            }

            is SyncOutcome.PermanentFailure -> {
                syncNotes.markInvalidItems(outcome.invalidItemIds)
                Result.failure()
            }
        }
    }
}

sealed interface SyncOutcome {
    data object Success : SyncOutcome

    data class TemporaryFailure(
        val reason: String
    ) : SyncOutcome

    data class PermanentFailure(
        val invalidItemIds: List<String>
    ) : SyncOutcome
}

Beim Erstellen der Arbeit würdest du zusätzlich eine Backoff-Strategie setzen. Der genaue Code hängt von deiner WorkManager-Konfiguration ab, aber die Absicht bleibt gleich: Ein erneuter Versuch soll nicht sofort in enger Schleife laufen.

val request = OneTimeWorkRequestBuilder<SyncNotesWorker>()
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        30,
        TimeUnit.SECONDS
    )
    .build()

In diesem Beispiel liegt die wichtigste Entscheidung nicht in der Syntax, sondern in der Fehlerordnung. TemporaryFailure führt zu Result.retry, aber nur bis zu einer Grenze. PermanentFailure führt nicht zu einem Retry, weil ein späterer Zeitpunkt falsche Daten nicht korrigiert. Genau diese Unterscheidung macht den Code belastbarer.

Eine typische Stolperfalle ist das blinde Abfangen aller Exceptions:

override suspend fun doWork(): Result {
    return try {
        syncNotes()
        Result.success()
    } catch (error: Exception) {
        Result.retry()
    }
}

Dieser Code wirkt auf den ersten Blick robust, ist aber oft zu grob. Er wiederholt auch Fehler, die du besser sofort sichtbar machen solltest. Wenn dein JSON-Mapping wegen eines Programmierfehlers scheitert, wird der Worker immer wieder laufen. Wenn der Server einen fachlichen Fehler meldet, wird er ebenfalls wiederholt. Das kostet Ressourcen und verschleiert die Ursache.

Besser ist eine klare Regel: Retry nur bei Fehlern, die außerhalb deiner fachlichen Kontrolle liegen und plausibel vorübergehend sind. Dazu gehören Netzwerk-Timeouts, temporäre Serverfehler oder kurzzeitig fehlende Verbindung. Kein Retry bei ungültigen Eingaben, fehlenden lokalen Daten, nicht behebbaren Berechtigungsproblemen oder Programmierfehlern.

Auch bei Coroutines solltest du sauber bleiben. Nutze suspendierende APIs, statt blockierende Aufrufe in Hintergrundthreads zu verstecken. Lass Cancellation zu, wo sie sinnvoll ist. Fange nicht pauschal CancellationException weg, denn Abbruch ist in Coroutine-Code ein normales Steuerungssignal. Wenn du Fehler behandelst, übersetze sie bewusst in Domänenergebnisse. Dadurch kann dein Worker eine stabile Entscheidung treffen.

Für Compose ist die wichtigste Praxisregel: Die UI startet nicht bei jedem Recomposing neue Retry-Logik. Sie beobachtet Status. Wenn du etwa eine Liste lokaler Sync-Zustände anzeigst, kommt dieser Zustand aus Repository oder Datenbank, gerne als Flow. Ein Button „Erneut versuchen“ kann neue Arbeit planen oder einen blockierten Datensatz freigeben. Die automatische Wiederholung gehört aber in die Hintergrundschicht, nicht in Composables.

Beim Testen kannst du dein Verständnis gut prüfen. Schreibe Tests für den Use Case, die temporäre und dauerhafte Fehler getrennt simulieren. Prüfe, ob ein Timeout als temporär eingeordnet wird. Prüfe, ob eine ungültige Serverantwort keinen Retry auslöst. In einem Worker-Test oder einer Code-Review achtest du dann auf drei Fragen: Gibt es eine Backoff-Strategie? Gibt es eine maximale oder fachlich begründete Grenze? Werden Fehlerarten klar unterschieden?

Beim Debuggen helfen strukturierte Logs. Logge nicht nur „Sync failed“, sondern den Typ der Entscheidung: temporärer Fehler, erneuter Versuch, dauerhafter Fehler, maximale Versuche erreicht. Achte dabei darauf, keine sensiblen Nutzerdaten zu protokollieren. Gute Logs machen sichtbar, ob dein System wiederholt, weil es soll, oder weil ein Fehler falsch eingeordnet wurde.

Eine weitere Stolperfalle ist die Verwechslung von Nutzerwunsch und Systemzustand. Wenn ein Nutzer aktiv auf „Senden“ tippt, erwartest du häufig direktes Feedback. Wenn derselbe Upload später im Hintergrund nachgeholt wird, ist ein geplanter Retry sinnvoll. Das Verhalten darf also vom Kontext abhängen. Trotzdem sollte die fachliche Fehlerklassifikation gleich bleiben. Ein ungültiger Datensatz bleibt ungültig, egal ob er aus einem Button-Klick oder aus einem Worker heraus verarbeitet wird.

Fazit

Work Retries machen deine Hintergrundarbeit widerstandsfähiger, wenn du sie gezielt einsetzt: Result.retry ist für temporäre Fehler gedacht, Backoff schützt vor unnötiger Last, und klare Fehlerklassifikation verhindert endlose Wiederholungen. Prüfe das Gelernte an einem echten Worker in deinem Projekt: Simuliere Offline-Modus, Serverfehler und ungültige Daten, beobachte Logs und Statuswerte, und kontrolliere im Code-Review, ob jeder Retry fachlich begründet ist.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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