Android Coden
Android 10 min lesen

Eindeutige Hintergrundarbeit in Android

Eindeutige Hintergrundarbeit verhindert doppelte Jobs und hält Sync-Abläufe auch bei wiederholten Auslösern stabil.

Wenn Nutzer mehrfach auf „Synchronisieren“ tippen, wenn ein Pull-to-refresh mehrfach feuert oder wenn ein Offline-First-Repository nach jeder lokalen Änderung einen Sync anstößt, darf deine App nicht unkontrolliert mehrere identische Hintergrundaufgaben starten. Unique Work beschreibt genau diese Schutzschicht: Eine Aufgabe bekommt eine fachliche Identität, und deine App entscheidet bewusst, was mit bereits geplanter oder laufender Arbeit derselben Art passieren soll.

Was ist das?

Unique Work bedeutet, dass du Hintergrundarbeit nicht nur als „starte einen Job“ behandelst, sondern als benannte, wiedererkennbare Arbeit. Statt fünfmal dieselbe Synchronisierung in die Warteschlange zu legen, sagst du: „Es gibt genau eine Arbeit mit dem Namen sync-notes.“ Wenn diese Arbeit schon existiert, greift eine Replacement Policy. Diese Policy legt fest, ob die vorhandene Arbeit bestehen bleibt, ob sie ersetzt wird oder ob neue Arbeit an eine bestehende Kette angehängt wird.

Im Android-Kontext ist das besonders wichtig, weil Hintergrundarbeit oft aus mehreren Richtungen angestoßen wird. In einer Compose-Oberfläche kann ein Nutzer einen Button drücken. Ein Repository kann nach einer Datenänderung einen Upload planen. Ein periodischer Sync kann ebenfalls aktiv werden. Zusätzlich können App-Starts, Netzwerkwechsel oder Retry-Mechanismen weitere Auslöser sein. Ohne eindeutige Arbeit entstehen schnell Duplikate: mehrere Uploads derselben Datei, mehrere Syncs derselben Tabelle oder mehrere Benachrichtigungen für denselben Status.

Das mentale Modell ist: Eine Hintergrundaufgabe hat eine technische Ausführung und eine fachliche Bedeutung. Die technische Ausführung ist der konkrete Worker, die Coroutine oder der Flow-Collector. Die fachliche Bedeutung ist zum Beispiel „alle offenen Änderungen zum Server senden“. Unique Work schützt diese fachliche Bedeutung vor mehrfacher paralleler Ausführung.

Das Thema passt direkt zu Coroutines, Flow und moderner Android-Architektur. Coroutines helfen dir, asynchrone Arbeit strukturiert auszuführen. Flow hilft dir, Zustände und Datenänderungen reaktiv zu beobachten. Die Data Layer ist der Ort, an dem du entscheidest, welche Arbeit wirklich notwendig ist. Unique Work ergänzt diese Bausteine, indem es Planung und Wiederholung kontrolliert. Es geht nicht darum, jede Wiederholung zu verhindern. Es geht darum, ungewollte Duplikate zu vermeiden und Wiederholungen so zu gestalten, dass sie korrekt bleiben.

Ein zentraler Begriff ist Idempotenz. Eine idempotente Aufgabe darf mehrfach ausgeführt werden, ohne den fachlichen Zustand falsch zu verändern. Ein Sync, der „setze Serverstatus auf diesen Wert“ sendet, ist meist leichter idempotent zu bauen als ein Sync, der „erhöhe Zähler um eins“ sendet. Unique Work reduziert Duplikate, ersetzt aber keine saubere Datenlogik. In guten Android-Apps arbeiten beide Ideen zusammen: Du planst nicht unnötig doppelt, und wenn Arbeit dennoch wiederholt wird, bleibt sie fachlich korrekt.

Wie funktioniert es?

In der Praxis setzt du Unique Work häufig mit Jetpack WorkManager um. WorkManager ist für aufschiebbare, zuverlässige Hintergrundarbeit gedacht, zum Beispiel Upload, Sync oder lokale Verarbeitung, die auch nach App-Neustarts fortgesetzt werden soll. Für eindeutige Arbeit vergibst du einen stabilen Namen. Dieser Name sollte nicht zufällig sein und nicht von technischen Details abhängen, die sich bei jedem Aufruf ändern. Er sollte ausdrücken, welche fachliche Aufgabe eindeutig sein soll.

Eine typische Entscheidung ist die Replacement Policy. Bei eindeutiger einmaliger Arbeit gibt es mehrere Denkweisen. KEEP bedeutet: Wenn diese Arbeit schon läuft oder geplant ist, bleibt sie bestehen, und der neue Auftrag wird ignoriert. Das passt gut zu „Sync alles“, weil ein bereits geplanter Sync die neuesten lokalen Daten aus der Datenbank lesen kann. REPLACE bedeutet: Alte Arbeit wird abgebrochen und durch neue Arbeit ersetzt. Das passt, wenn die neue Anfrage präzisere oder aktuellere Eingabedaten enthält und die alte Arbeit nicht mehr sinnvoll ist. APPEND hängt neue Arbeit an eine bestehende eindeutige Kette an. Das ist hilfreich, wenn Reihenfolge wichtig ist. Je nach API-Version und konkreter WorkManager-Variante gibt es außerdem feinere Optionen, die fehlschlagende Ketten unterschiedlich behandeln.

Für Lernende ist wichtig: Die Policy ist keine Dekoration. Sie ist eine fachliche Entscheidung. Wenn du REPLACE verwendest, kann laufende Arbeit abgebrochen werden. Das ist problematisch, wenn der Worker gerade einen mehrstufigen Upload ohne saubere Zwischenstände ausführt. Wenn du KEEP verwendest, können neue Eingabedaten verloren wirken, wenn dein Worker nur die beim Planen übergebenen Daten nutzt. Deshalb ist für Sync-Work oft eine robuste Regel sinnvoll: Übergib möglichst wenig flüchtige Details an den Worker, und lasse ihn den aktuellen Zustand aus der lokalen Datenquelle lesen. Dann kann ein einziger geplanter Sync mehrere inzwischen entstandene Änderungen erfassen.

Coroutines sind dabei die Ausführungsform, nicht die Eindeutigkeitsgarantie. Ein CoroutineWorker kann suspendierende Repository-Funktionen aufrufen. Die Best Practices für Coroutines bleiben relevant: blockiere nicht den Main Thread, injiziere Dispatcher bei Bedarf, halte Arbeit abbrechbar und lege Verantwortlichkeiten in passende Schichten. Unique Work sitzt darüber: Es entscheidet, wie oft diese Coroutine-Arbeit überhaupt geplant wird.

Flow kommt ins Spiel, wenn du Änderungen beobachtest. Eine lokale Datenbank kann einen Flow mit ausstehenden Änderungen liefern. Dein UI kann diesen Zustand anzeigen. Dein Repository kann aus einer Aktion heraus einen Sync planen. Trotzdem solltest du nicht bei jedem Flow-Emission unreflektiert neue Hintergrundarbeit starten. Ein Flow kann häufig emittieren: nach Inserts, Updates, Konfliktauflösungen oder Statusänderungen. Wenn jede Emission ohne eindeutigen Namen einen neuen Worker erzeugt, baust du eine Duplikatmaschine. Besser ist, aus relevanten Zustandsübergängen eine eindeutige Arbeit zu planen.

In einer Offline-First-App ist Unique Work besonders wertvoll. Der lokale Zustand ist die Quelle, mit der das UI arbeitet. Netzwerkoperationen holen auf, sobald Verbindung und Bedingungen passen. Wenn ein Nutzer offline zehn Notizen ändert, brauchst du nicht zehn parallele Vollsyncs. Du brauchst eine zuverlässige Arbeit, die später alle offenen Änderungen verarbeitet. Gleichzeitig muss dein Sync idempotent sein: Wenn der Worker nach einem Prozessabbruch erneut startet, darf er nicht dieselben Änderungen doppelt als neue Serveraktionen interpretieren. Dafür brauchst du stabile IDs, Sync-Status in der lokalen Datenbank und Serveroperationen, die Wiederholung vertragen.

Die Datenebene ist der richtige Ort für diese Logik. Eine Compose-Schaltfläche sollte nicht wissen, welche Replacement Policy fachlich korrekt ist. Sie ruft zum Beispiel syncRepository.requestSync() auf. Das Repository oder ein eigener Scheduler entscheidet dann, welcher eindeutige Work-Name verwendet wird, welche Constraints gelten und welche Policy passt. Dadurch bleibt die UI dünn, und du kannst die Regeln in Tests und Code-Reviews gezielt prüfen.

In der Praxis

Stell dir eine Notizen-App vor. Nutzer können Notizen offline bearbeiten. Jede Änderung wird lokal gespeichert und als „ausstehend“ markiert. Ein Sync soll diese Änderungen später zum Server senden. Der Sync kann durch einen Button, durch App-Start oder durch eine Repository-Aktion ausgelöst werden. Fachlich brauchst du aber nicht mehrere parallele Syncs. Ein Sync, der die lokale Datenbank liest, reicht.

Ein möglicher Scheduler sieht so aus:

class NotesSyncScheduler(
    private val workManager: WorkManager
) {
    fun requestSync() {
        val request = OneTimeWorkRequestBuilder<NotesSyncWorker>()
            .setConstraints(
                Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.CONNECTED)
                    .build()
            )
            .addTag("notes-sync")
            .build()

        workManager.enqueueUniqueWork(
            "sync-notes",
            ExistingWorkPolicy.KEEP,
            request
        )
    }
}

Der Name "sync-notes" ist hier die fachliche Identität. KEEP sagt: Wenn schon ein Sync geplant ist oder läuft, brauchen wir keinen zweiten. Diese Entscheidung funktioniert gut, wenn NotesSyncWorker beim Start die aktuellen ausstehenden Änderungen aus der lokalen Datenbank liest. Dann ist es egal, ob während der Wartezeit noch weitere Notizen geändert wurden. Der eine Worker kann alle offenen Änderungen verarbeiten, die zum Ausführungszeitpunkt sichtbar sind.

Der Worker könnte stark vereinfacht so aussehen:

class NotesSyncWorker(
    appContext: Context,
    params: WorkerParameters,
    private val repository: NotesRepository
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        return try {
            repository.syncPendingNotes()
            Result.success()
        } catch (exception: IOException) {
            Result.retry()
        } catch (exception: SyncConflictException) {
            Result.failure()
        }
    }
}

Die Details liegen im Repository. Dort sollte nicht blind „sende alles noch einmal“ passieren. Eine solide Implementierung speichert pro Änderung eine stabile lokale ID, einen Sync-Status und eventuell eine Server-ID oder Version. Nach einem erfolgreichen Upload wird der lokale Status aktualisiert. Wenn der Worker nach einem Retry dieselbe Änderung erneut sieht, sollte die Server-API damit umgehen können oder das Repository muss erkennen, dass die Änderung bereits verarbeitet wurde. Das ist Idempotenz im Arbeitsalltag: Wiederholung ist erlaubt, aber sie erzeugt keinen falschen Zustand.

Eine klare Entscheidungsregel lautet: Nutze KEEP, wenn die bestehende Arbeit beim Ausführen den neuesten Zustand selbst lesen kann. Nutze REPLACE, wenn die neue Anfrage die alte fachlich überholt und Abbruch sicher ist. Nutze Anhängen, wenn mehrere Schritte in Reihenfolge abgearbeitet werden müssen. Entscheide nicht nach Gefühl, sondern nach Datenfluss: Wo liegt der aktuelle Zustand, und was passiert bei Abbruch, Retry oder mehrfacher Planung?

Eine typische Stolperfalle ist, eindeutige Arbeit mit eindeutigen Eingabedaten zu verwechseln. Wenn du bei jedem Buttondruck einen anderen Work-Namen erzeugst, etwa "sync-notes-${System.currentTimeMillis()}", hast du keine Unique Work mehr. Du hast nur viele benannte Einzeljobs. Ebenso riskant ist es, große Datenmengen als InputData an den Worker zu übergeben und dann KEEP zu verwenden. Wenn der erste Worker alte Eingaben hat und weitere Anfragen ignoriert werden, verarbeitet er möglicherweise nicht das, was du erwartest. Für Sync-Aufgaben ist es oft besser, IDs oder gar keine Nutzdaten zu übergeben und den aktuellen Zustand aus der Datenbank zu laden.

Eine zweite Stolperfalle betrifft Compose. In Compose werden Funktionen häufig neu ausgeführt. Wenn du in einer Composable direkt beim Rendern Arbeit planst, kann das mehrfach passieren. Hintergrundarbeit gehört hinter ein Ereignis oder in eine kontrollierte Nebenwirkung. Ein Button darf viewModel.requestSync() auslösen. Ein LaunchedEffect braucht einen stabilen Key und eine klare Bedingung. Auch dann sollte die eigentliche Planung in der Data Layer oder in einem Scheduler gekapselt sein. Compose ist für UI-Zustand zuständig, nicht für die fachliche Eindeutigkeit deiner Sync-Jobs.

Auch bei Flow musst du vorsichtig sein. Angenommen, du beobachtest pendingChangesFlow. Bei jeder Änderung rufst du requestSync() auf. Mit Unique Work und KEEP ist das weniger gefährlich als ohne, weil nicht sofort viele parallele Jobs entstehen. Trotzdem solltest du prüfen, ob die Emission wirklich einen neuen Sync-Wunsch bedeutet. Manchmal reicht es, beim Wechsel von „keine ausstehenden Änderungen“ zu „es gibt ausstehende Änderungen“ zu planen. In anderen Fällen ist häufiges Planen akzeptabel, weil die Unique-Policy den Schaden begrenzt. Die fachliche Klarheit muss trotzdem im Code sichtbar sein.

Für Tests kannst du die Entscheidung klein schneiden. Teste nicht zuerst das gesamte Android-System. Teste, dass dein Repository bei wiederholtem requestSync() denselben eindeutigen Namen und die richtige Policy verwendet. In Instrumentation- oder Integrationsnähe kannst du prüfen, dass mehrere schnelle Nutzeraktionen nicht mehrere parallele Sync-Worker erzeugen. Zusätzlich solltest du das Verhalten des Workers selbst testen: Was passiert, wenn syncPendingNotes() beim ersten Versuch mit einem Netzwerkfehler abbricht und später erneut läuft? Werden bereits erfolgreich synchronisierte Einträge übersprungen? Bleiben Konflikte nachvollziehbar?

Beim Debugging helfen klare Logs, aber sie müssen fachlich benannt sein. Logge nicht nur „Worker started“, sondern zum Beispiel den Work-Namen, die Anzahl ausstehender Änderungen und das Ergebnis. In WorkManager kannst du außerdem WorkInfo beobachten, um Status wie ENQUEUED, RUNNING, SUCCEEDED oder FAILED sichtbar zu machen. Für Lernzwecke ist es nützlich, absichtlich fünfmal schnell auf den Sync-Button zu drücken und zu prüfen, ob nur eine eindeutige Arbeit aktiv bleibt. Danach kannst du während laufender Arbeit eine weitere lokale Änderung erzeugen und kontrollieren, ob sie im nächsten oder gleichen Sync korrekt verarbeitet wird.

Im Code-Review solltest du bei Unique Work gezielt nach drei Fragen suchen. Erstens: Ist der eindeutige Name stabil und fachlich passend? Zweitens: Passt die Replacement Policy zum Datenfluss und zu möglichen Abbrüchen? Drittens: Ist die Arbeit idempotent oder verlässt sie sich darauf, dass sie wirklich nie wiederholt wird? Die dritte Frage ist entscheidend, weil Android-Hintergrundarbeit durch Prozessende, Netzwerkfehler und Retry-Logik mehrfach ausgeführt werden kann. Eine App, die nur bei idealer Ausführung korrekt bleibt, ist im Alltag zu fragil.

Eine saubere Architektur macht diese Fragen leichter. Die UI sendet Absichten, das ViewModel koordiniert UI-nahe Aktionen, die Data Layer kennt Datenzustand und Sync-Regeln, und der Worker führt zuverlässige Hintergrundarbeit aus. Unique Work ist dann kein verstreuter Trick in mehreren Screens, sondern eine klare Regel an einer Stelle. Das erhöht die Qualität, weil du Duplikate nicht an jeder Oberfläche einzeln bekämpfen musst.

Fazit

Unique Work hilft dir, wiederholte Auslöser in Android-Apps kontrolliert zu behandeln: Nutzeraktionen, Flow-Emissionen, Offline-First-Syncs und Retry-Mechanismen führen nicht automatisch zu doppelter Hintergrundarbeit. Entscheidend sind ein stabiler fachlicher Work-Name, eine bewusst gewählte Replacement Policy und idempotente Verarbeitung in der Datenebene. Prüfe dein Verständnis aktiv: Baue einen kleinen Sync-Button, drücke ihn mehrfach schnell hintereinander, beobachte WorkInfo und Logs, simuliere Netzwerkfehler und lies deinen Code danach mit den drei Review-Fragen zu Name, Policy und Idempotenz.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

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