Android Coden
Android 8 min lesen

Work Input und Output in Android-Background-Work

Workers brauchen klare Eingaben und Ergebnisse. Du lernst, kleine strukturierte Werte sicher zu übergeben.

Wenn du Hintergrundarbeit in Android baust, reicht es nicht, nur einen Job zu starten. Du musst auch festlegen, welche Daten dieser Job bekommt, wie er daraus ein Ergebnis erzeugt und wie ein nachfolgender Job dieses Ergebnis wieder sicher lesen kann. Genau darum geht es bei Work Input und Output: kleine, strukturierte Werte werden zwischen Arbeitsschritten transportiert, ohne dass du UI-Zustand, große Objekte oder unsichere Seiteneffekte mitschleppst.

Was ist das?

Work Input und Output ist der Datenvertrag eines Hintergrundjobs. Input ist das, was ein Worker vor dem Start erhält. Output ist das, was er nach erfolgreicher Arbeit an das System zurückgibt. Im Android-Kontext denkst du dabei meist an Hintergrundarbeit, die nicht direkt an einen Screen gebunden ist: Uploads, Synchronisation, Bildverarbeitung, Cache-Aufbau oder das Nachbereiten lokal gespeicherter Daten.

Das mentale Modell ist: Ein Worker ist kein Funktionsaufruf mit beliebigen Kotlin-Parametern, sondern ein isolierter Arbeitsschritt. Er kann später laufen, erneut gestartet werden oder nach einem Prozessneustart fortgesetzt werden. Deshalb braucht er Daten, die klein, stabil und serialisierbar sind. Statt ein komplettes User-Objekt weiterzureichen, übergibst du zum Beispiel eine userId. Statt ein Bitmap in den Input zu packen, speicherst du die Datei und übergibst nur eine Uri oder einen Dateipfad, je nach Architektur und Berechtigungslage.

Für Lernende ist der wichtigste Punkt: Input und Output sind nicht deine Datenbank und nicht dein ViewModel. Sie sind eher wie ein Lieferschein. Darauf steht, welche Ressource bearbeitet werden soll und welches kleine Ergebnis dabei herauskam. Die eigentlichen Daten liegen in Room, DataStore, dem Dateisystem oder auf dem Server. Das passt gut zu moderner Android-Architektur, weil UI, Repository, Worker und Datenquelle klar getrennt bleiben.

Im Umfeld von Kotlin, Coroutines und Flow bedeutet das: Hintergrundarbeit kann suspendierende APIs verwenden, Fortschritt kann beobachtbar gemacht werden, und Ergebnisse können in deine App-Schichten zurückfließen. Trotzdem bleibt der Datentransport zwischen Work-Schritten bewusst schmal. Das schützt dich vor schwer testbarem Code und vor Fehlern, die erst auftreten, wenn Android deine App im Hintergrund anders behandelt als im Vordergrund.

Wie funktioniert es?

Ein Worker liest seine Eingaben aus einem strukturierten Datencontainer. In WorkManager ist das typischerweise Data. Dieser Container kann einfache Werte speichern, etwa String, Int, Long, Boolean, Double und Arrays solcher Typen. Das klingt eingeschränkt, ist aber Absicht. Hintergrundarbeit muss robust sein, auch wenn der Prozess beendet wurde. Ein kleiner Satz einfacher Typen lässt sich zuverlässig speichern und wiederherstellen.

Du definierst dafür Schlüssel. Diese Schlüssel sind Teil deines Vertrags. Wenn ein Worker "photo_uri" erwartet, muss der aufrufende Code denselben Schlüssel setzen. Wenn ein nachfolgender Worker "remote_id" lesen soll, muss der vorherige Worker diesen Output wirklich schreiben. Schon kleine Tippfehler können dazu führen, dass dein Worker mit leeren Werten arbeitet oder mit einem Fehler abbricht. Deshalb gehören solche Schlüssel als Konstanten an eine klare Stelle, oft direkt in ein Companion Object des Workers oder in ein kleines Contract-Objekt.

Bei Worker-Ketten wird der Output eines Schritts zum Input eines späteren Schritts. Das ist nützlich, wenn du Arbeit in fachliche Etappen teilst: zuerst eine Datei komprimieren, dann hochladen, dann den lokalen Datensatz als synchronisiert markieren. Jeder Schritt bekommt nur das, was er wirklich braucht. Dadurch bleibt die Kette nachvollziehbar. Du kannst jeden Worker einzeln testen und im Code-Review prüfen, ob die Übergaben stimmen.

Coroutines passen hier hinein, weil viele Hintergrundjobs I/O-lastig sind. Ein CoroutineWorker kann suspendierende Repository-Methoden aufrufen, ohne Callbacks zu verschachteln. Die Coroutine-Best-Practices bleiben wichtig: Blockiere keine Threads unnötig, verstecke Dispatching nicht chaotisch in der UI-Schicht, und sorge dafür, dass deine Datenoperationen testbar bleiben. Wenn du Ergebnisse später im UI zeigen willst, ist Flow oft die bessere Verbindung zwischen Datenquelle und Oberfläche. Der Worker schreibt zum Beispiel einen Status in die Datenbank, und dein Compose-Screen beobachtet diesen Status über einen Flow aus dem Repository.

Wichtig ist die Trennung zwischen Job-Ergebnis und App-Zustand. Der Output eines Workers sagt etwa: “Upload erfolgreich, Server-ID lautet 123”. Der dauerhafte Zustand sollte anschließend in einer Datenquelle liegen. Wenn deine UI wissen muss, ob ein Foto hochgeladen wurde, sollte sie das nicht nur aus dem einmaligen Worker-Output lesen. Sie sollte eine persistente Wahrheit beobachten, zum Beispiel eine Spalte syncState in Room.

Auch Fehler gehören zum Vertrag. Ein Worker kann erfolgreich sein, wiederholt werden oder fehlschlagen. Output ist vor allem für erfolgreiche Ergebnisse sinnvoll. Für Retry-Entscheidungen brauchst du oft gar keinen Output, sondern eine saubere Fehlerbehandlung: War das Netzwerk kurz weg, kann ein Retry passen. Ist der Input ungültig, sollte der Worker kontrolliert fehlschlagen und den Fehler nachvollziehbar loggen. Du vermeidest dadurch Ketten, die mit kaputten Werten weiterlaufen.

In der Praxis

Stell dir vor, deine App speichert Notizen mit optionalem Foto. Das Foto wird lokal erzeugt und später im Hintergrund hochgeladen. Danach soll die lokale Notiz die Server-ID des Fotos kennen. Du könntest einen Worker schreiben, der eine lokale Foto-Uri und eine Notiz-ID bekommt. Er lädt das Foto hoch, erhält eine Remote-ID und gibt diese als Output zurück. Ein nachfolgender Worker kann damit die lokale Datenbank aktualisieren.

Kotlin-Beispiel:

class UploadPhotoWorker(
    appContext: Context,
    params: WorkerParameters,
    private val repository: PhotoRepository
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        val noteId = inputData.getLong(KEY_NOTE_ID, -1L)
        val photoUri = inputData.getString(KEY_PHOTO_URI)

        if (noteId <= 0L || photoUri.isNullOrBlank()) {
            return Result.failure()
        }

        return try {
            val remoteId = repository.uploadPhoto(
                noteId = noteId,
                localUri = photoUri
            )

            val output = workDataOf(
                KEY_NOTE_ID to noteId,
                KEY_REMOTE_PHOTO_ID to remoteId
            )

            Result.success(output)
        } catch (e: IOException) {
            Result.retry()
        } catch (e: IllegalArgumentException) {
            Result.failure()
        }
    }

    companion object {
        const val KEY_NOTE_ID = "note_id"
        const val KEY_PHOTO_URI = "photo_uri"
        const val KEY_REMOTE_PHOTO_ID = "remote_photo_id"
    }
}

Der aufrufende Code baut den Input bewusst knapp:

val request = OneTimeWorkRequestBuilder<UploadPhotoWorker>()
    .setInputData(
        workDataOf(
            UploadPhotoWorker.KEY_NOTE_ID to noteId,
            UploadPhotoWorker.KEY_PHOTO_URI to photoUri.toString()
        )
    )
    .build()

Hier erkennst du mehrere Regeln. Erstens: Es werden nur primitive oder einfache Werte übergeben. Die Notiz selbst bleibt in der Datenbank. Das Foto bleibt als Datei erhalten. Zweitens: Der Worker validiert seinen Input direkt am Anfang. Ein fehlender Wert wird nicht still akzeptiert. Drittens: Das Ergebnis ist ebenfalls klein. Die Remote-ID reicht, um den nächsten Schritt oder das Repository zu informieren.

Wenn du eine Kette baust, sollte jeder Schritt einen klaren Zweck haben. Ein möglicher Ablauf wäre: CompressPhotoWorker erzeugt eine kleinere Datei und gibt deren Uri aus. UploadPhotoWorker lädt diese Datei hoch und gibt die Remote-ID aus. MarkPhotoSyncedWorker schreibt den endgültigen Status in Room. Die Namen zeigen bereits, welche Verantwortung ein Worker hat. Das hilft beim Debuggen, weil du in WorkManager-Logs und Tests erkennen kannst, welcher Abschnitt gebrochen ist.

Eine typische Stolperfalle ist, Output als bequemen Ersatz für Architektur zu verwenden. Dann werden immer mehr Werte in Data gepackt: Titel, Beschreibung, Nutzername, komplette JSON-Strukturen und technische Flags. Kurz wirkt das praktisch, später wird es spröde. Schlüssel ändern sich, alte WorkRequests liegen vielleicht noch in der Warteschlange, und neue Worker-Versionen erwarten andere Daten. Besser ist eine stabile ID als Input und ein Repository, das den aktuellen Zustand aus einer echten Datenquelle lädt.

Eine zweite Stolperfalle ist unklare Fehlersemantik. Wenn ein Worker bei fehlender photoUri Result.retry() zurückgibt, wird Android denselben ungültigen Job erneut versuchen. Das kostet Akku, verzögert andere Arbeit und verdeckt den eigentlichen Fehler. Für ungültigen Input ist failure meist passender. Für temporäre Infrastrukturprobleme wie Netzwerkfehler ist retry sinnvoller. Diese Unterscheidung solltest du im Code sichtbar machen.

Eine dritte Stolperfalle betrifft Typen und Schlüssel. getLong(KEY_NOTE_ID, -1L) gibt dir immer einen Wert, auch wenn der echte Input fehlt. Der Default-Wert darf nicht zufällig wie ein gültiger Wert behandelt werden. Nutze daher Werte, die du eindeutig validieren kannst, oder kapsle das Lesen in eine kleine Funktion. In größeren Projekten lohnt sich ein eigener Contract:

object PhotoWorkContract {
    const val NOTE_ID = "note_id"
    const val PHOTO_URI = "photo_uri"
    const val REMOTE_PHOTO_ID = "remote_photo_id"
}

Noch stabiler wird es, wenn du im Test prüfst, dass der Worker bei fehlenden Daten fehlschlägt und bei gültigen Daten den erwarteten Output schreibt. Du musst dafür nicht die komplette App starten. Du kannst Repository-Abhängigkeiten ersetzen, kontrollierte Inputs setzen und das Ergebnis auswerten. Das ist ein guter Schritt vom Anfänger-Code zum professionelleren Android-Code: Hintergrundarbeit wird nicht nur manuell ausprobiert, sondern mit klaren Annahmen geprüft.

In Compose taucht dieses Thema oft indirekt auf. Der Screen startet nicht selbst lange Arbeit in einer Composable-Funktion. Stattdessen löst die UI eine Aktion im ViewModel aus. Das ViewModel oder eine Use-Case-Schicht plant den WorkRequest. Der Screen beobachtet dann einen Zustand, zum Beispiel über Flow aus der Datenbank. So bleibt Compose für Darstellung zuständig, während Worker und Repository die Arbeit erledigen. Work Input und Output verbinden die Arbeitsschritte, nicht die UI direkt mit dem Hintergrundsystem.

Eine praktische Entscheidungsregel lautet: Übergib an Worker nur Werte, die du auch in einem Logeintrag sicher und verständlich erklären könntest. “noteId=42, photoUri=content://…” ist nachvollziehbar. Ein komplettes verschachteltes JSON mit UI-Zustand ist ein Warnsignal. Wenn du mehr Kontext brauchst, speichere ihn persistiert und übergib nur den Schlüssel dorthin.

Fazit

Work Input und Output hilft dir, Hintergrundarbeit als saubere Kette kleiner Verträge zu denken: Jeder Worker bekommt wenige strukturierte Daten, validiert sie, erledigt genau eine Aufgabe und gibt nur das notwendige Ergebnis zurück. Prüfe das aktiv in deinem eigenen Code: Schreibe einen Worker mit absichtlich fehlendem Input, beobachte das Ergebnis im Debugger, ergänze einen Test für Erfolg und Fehlerfall, und achte im Code-Review besonders auf Schlüssel, Datengröße und Retry-Logik. So lernst du nicht nur eine API, sondern eine robuste Denkweise für Android-Apps, die auch unter echten Hintergrundbedingungen zuverlässig bleiben.

Quellen (3)
Redaktion

Geschrieben von

Redaktion

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