Flow-Operatoren in Kotlin Flow
Flow-Operatoren formen Datenströme für deine UI. Du lernst map, filter, combine und transform gezielt einzusetzen.
Flow-Operatoren sind ein zentrales Werkzeug, wenn du in modernen Android-Apps mit Kotlin Flow arbeitest. Sie helfen dir, laufende Datenströme so zu formen, dass deine UI nicht mit Rohdaten, Nebenbedingungen und Umrechnungen überladen wird. Statt viele einzelne Zwischenschritte in Callback-Code oder Composables zu verteilen, beschreibst du eine Pipeline: Daten kommen hinein, werden gefiltert, umgewandelt, kombiniert und als klarer UI-Zustand weitergegeben.
Was ist das?
Flow-Operatoren sind Funktionen, die auf einem Flow aufgerufen werden und daraus wieder einen neuen Flow machen. Der ursprüngliche Strom wird dabei nicht direkt verändert. Du beschreibst nur, wie Werte verarbeitet werden sollen, sobald jemand den Flow sammelt. Dieses Verhalten passt gut zum Android-Alltag, weil viele Daten nicht einmalig vorhanden sind: Datenbankeinträge ändern sich, Netzwerkergebnisse treffen später ein, Nutzer ändern Filter, der Login-Zustand wechselt, und die UI muss darauf reagieren.
Das mentale Modell ist: Ein Flow ist ein Strom aus Werten über Zeit. Ein Operator ist eine Station in diesem Strom. map übersetzt jeden Wert in eine andere Form. filter lässt nur passende Werte weiter. combine nimmt Werte aus mehreren Flows und baut daraus einen gemeinsamen Zustand. transform ist flexibler und kann aus einem Eingangswert keinen, einen oder mehrere Ausgangswerte erzeugen.
Für Android ist das besonders nützlich, weil ViewModels häufig zwischen Datenquellen und UI vermitteln. Ein Repository liefert vielleicht Datenbankmodelle, ein weiterer Flow liefert Einstellungen, und die UI braucht daraus einen UiState. Mit Flow-Operatoren hältst du diese Übersetzung lesbar. Du trennst fachliche Verarbeitung von Darstellungscode und machst die Logik testbarer.
Wie funktioniert es?
Die meisten Flow-Operatoren sind lazy. Das bedeutet: Die Pipeline läuft erst, wenn der Flow gesammelt wird, zum Beispiel im ViewModel oder über Compose mit einer lifecycle-bewussten Collect-Funktion. Vorher ist die Operator-Kette nur eine Beschreibung. Das schützt vor unnötiger Arbeit und passt zum Coroutine-Modell von Kotlin.
map nutzt du, wenn jeder eingehende Wert genau in einen neuen Wert übersetzt werden soll. Ein typisches Beispiel ist die Umwandlung von Datenbank-Entities in UI-Modelle. filter nutzt du, wenn Werte nur unter einer Bedingung relevant sind. Das kann ein Suchtext sein, ein Aktiv-Status oder eine Nutzerberechtigung. combine ist sinnvoll, wenn der Zielzustand von mehreren Quellen abhängt. Sobald einer der beteiligten Flows einen neuen Wert liefert, wird mit den neuesten bekannten Werten der anderen Flows neu gerechnet. transform verwendest du, wenn map zu starr ist, etwa wenn du zuerst einen Ladezustand ausgeben und danach ein Ergebnis emitten möchtest.
Wichtig ist auch die Frage, wo diese Operatoren stehen. In einer sauberen Android-Architektur liegen einfache Datenumformungen oft im Repository oder Use Case. UI-nahe Zustände wie isLoading, emptyMessage oder sichtbare Aktionsbuttons entstehen häufig im ViewModel. Direkt in Composables solltest du Flow-Pipelines nur sehr zurückhaltend bauen. Composables können oft neu ausgeführt werden; komplexe Verarbeitung gehört daher besser in stabile Schichten, die du getrennt testen kannst.
Ein weiterer Punkt ist der Ausführungskontext. Operatoren laufen grundsätzlich im Kontext des Collectors, sofern du ihn nicht mit Mechanismen wie flowOn veränderst. Teure Arbeit, etwa Sortierung großer Listen oder aufwendige Mappings, sollte nicht unbedacht auf dem Main Thread landen. Für normale UI-Modelle ist das meist unkritisch, aber du solltest im Code-Review darauf achten, ob eine Pipeline viel CPU-Arbeit enthält.
In der Praxis
Stell dir eine Aufgaben-App vor. Die Datenbank liefert alle Aufgaben als Flow, ein zweiter Flow enthält den aktuellen Suchtext, und ein dritter Flow sagt, ob erledigte Aufgaben angezeigt werden sollen. Die UI braucht keine drei getrennten Streams, sondern einen stabilen Zustand mit einer fertigen Liste.
data class TaskEntity(
val id: Long,
val title: String,
val done: Boolean
)
data class TaskItemUi(
val id: Long,
val title: String,
val checked: Boolean
)
data class TaskListUiState(
val items: List<TaskItemUi>,
val isEmpty: Boolean
)
class TaskViewModel(
taskRepository: TaskRepository,
settingsRepository: SettingsRepository
) : ViewModel() {
private val searchQuery: MutableStateFlow<String> = MutableStateFlow("")
val uiState: StateFlow<TaskListUiState> =
combine(
taskRepository.observeTasks(),
searchQuery,
settingsRepository.showDoneTasks
) { tasks, query, showDone ->
tasks
.asSequence()
.filter { task -> showDone || !task.done }
.filter { task ->
query.isBlank() ||
task.title.contains(query, ignoreCase = true)
}
.map { task ->
TaskItemUi(
id = task.id,
title = task.title,
checked = task.done
)
}
.toList()
}
.map { items ->
TaskListUiState(
items = items,
isEmpty = items.isEmpty()
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskListUiState(
items = emptyList(),
isEmpty = true
)
)
fun onSearchQueryChanged(value: String) {
searchQuery.value = value
}
}
In diesem Beispiel übernimmt combine die Zusammenführung mehrerer Quellen. Danach formen filter und map die Daten in eine UI-taugliche Liste. Die UI muss nicht wissen, ob die Liste wegen des Suchtexts, wegen einer Einstellung oder wegen der Datenbank leer ist. Sie bekommt einen klaren TaskListUiState.
Eine praktische Entscheidungsregel: Nutze den spezifischsten Operator, der deine Absicht klar ausdrückt. Wenn du jeden Wert nur umrechnest, nimm map. Wenn du Werte ausschließen willst, nimm filter. Wenn mehrere Flows gemeinsam einen Zustand bilden, nimm combine. Greife zu transform, wenn du bewusst mehrere Emissionen oder bedingte Emissionen aus einem Eingangswert brauchst.
Eine typische Stolperfalle ist zu viel Logik in einer einzigen langen Pipeline. Flow-Operatoren machen Code lesbar, solange jeder Schritt eine klare Aufgabe hat. Wenn du verschachtelte Bedingungen, mehrere Datenquellen und Fehlerzustände in eine einzige Kette packst, wird der Code schwer prüfbar. Dann lohnt es sich, kleine private Funktionen zu extrahieren, etwa TaskEntity.toUi() oder filterTasks(...).
Eine zweite Stolperfalle betrifft Seiteneffekte. Operatoren wie map sollten möglichst reine Umformungen enthalten. Netzwerkaufrufe, Datenbankschreibvorgänge oder Navigation gehören dort nicht beiläufig hinein. Wenn eine Pipeline Nebenwirkungen auslöst, wird es schwieriger, Timing-Probleme und doppelte Ausführungen zu verstehen.
Testen kannst du solche Pipelines gut mit kontrollierten Flows. Gib feste Listen, Suchtexte und Einstellungen hinein und prüfe, welcher UiState herauskommt. Für Teams ist das auch ein guter Kandidat für automatisierte Tests in der Continuous Integration: Wenn jemand später die Filterlogik ändert, sollte ein Test sofort zeigen, ob erledigte Aufgaben plötzlich falsch angezeigt werden.
Fazit
Flow-Operatoren helfen dir, reaktive Android-Daten lesbar in UI-Zustand zu übersetzen. Baue dir beim Lernen das Bild einer Pipeline auf: map formt Werte um, filter reduziert sie, combine verbindet Quellen, und transform deckt Spezialfälle ab. Prüfe dein Verständnis aktiv, indem du eine kleine ViewModel-Pipeline schreibst, sie im Debugger Schritt für Schritt beobachtest und mit Tests absicherst, welche Werte bei bestimmten Eingaben wirklich in der UI ankommen.