Android Coden
Android 8 min lesen

Sequences in Kotlin

Sequences verarbeiten Kotlin-Daten verzögert und sparen Arbeit bei langen Ketten. Du lernst, wann sie Android-Code messbar entlasten.

Sequences sind ein Kotlin-Werkzeug für verzögerte Verarbeitung von Daten. Du nutzt sie, wenn du mehrere Schritte wie Filtern, Umwandeln und Begrenzen zu einer Pipeline verbindest und vermeiden möchtest, dass Kotlin nach jedem Schritt sofort eine neue Zwischenliste baut. In Android ist das vor allem dann interessant, wenn Datenmengen wachsen, wenn Transformationen teuer sind oder wenn Code im ViewModel, Repository oder in einer Mapper-Schicht regelmäßig ausgeführt wird.

Was ist das?

Eine Sequence ist eine Datenfolge, die ihre Elemente nicht sofort komplett verarbeitet. Stattdessen wird jedes Element erst dann durch die Verarbeitungskette geschickt, wenn ein Endergebnis abgefragt wird. Dieses Prinzip nennt man lazy evaluation: Die Arbeit passiert so spät wie möglich und nur so weit wie nötig.

Der Unterschied zu einer normalen List ist wichtig. Wenn du auf einer Liste filter, danach map und danach take aufrufst, erzeugen viele Collection-Operationen jeweils ein Zwischenergebnis. Bei kleinen Datenmengen ist das meist unkritisch. Bei langen Ketten oder großen Listen kann es aber unnötige Speicherarbeit bedeuten. Eine Sequence verbindet die Schritte zu einer Pipeline und verarbeitet Element für Element durch diese Pipeline.

Für Android-Lernende ist das ein Baustein auf dem Weg zu sauberem Kotlin-Code mit Performance-Bewusstsein. Du musst nicht jede Liste sofort in eine Sequence umwandeln. Du solltest aber erkennen, wann normale Collection-Verarbeitung unnötig viel Arbeit macht. Das passt gut zu modernem Android mit Kotlin, Jetpack und klaren Architekturschichten: Daten werden oft aus Datenbanken, Netzwerkanfragen oder lokalen Modellen gelesen und danach für UI-State, Listenansichten oder Compose-Screens vorbereitet.

Eine Sequence ist kein Ersatz für Coroutines, Flow oder Paging. Sie ist ein lokales Werkzeug für synchrone Datenverarbeitung innerhalb deines Codes. Wenn Daten über Zeit ankommen, etwa aus einer Datenbankbeobachtung oder einem Netzwerkstream, ist Flow oft das passendere Modell. Wenn du aber bereits eine Sammlung im Speicher hast und eine effiziente Transformationskette bauen willst, ist Sequence ein präzises Werkzeug.

Wie funktioniert es?

Das mentale Modell ist eine Produktionslinie. Bei einer normalen Liste läuft oft erst die komplette erste Station für alle Elemente, dann die komplette zweite Station und dann die dritte. Bei einer Sequence läuft ein einzelnes Element durch alle Stationen, danach das nächste Element. Dadurch kann Kotlin früher stoppen, wenn ein Ergebnis bereits reicht.

Eine Sequence entsteht zum Beispiel mit asSequence() aus einer bestehenden Collection oder mit sequenceOf(...). Danach kannst du bekannte Operationen wie filter, map, distinct, sortedBy, take oder firstOrNull verwenden. Viele dieser Operationen sind intermediate operations. Sie beschreiben nur einen weiteren Schritt der Pipeline. Ausgeführt wird die Pipeline erst durch eine terminal operation, also etwa toList(), first(), count(), sumOf() oder forEach().

Das ist der Kern: Eine Sequence ist eine Beschreibung bis zur Terminal-Operation. Ohne Terminal-Operation passiert keine Verarbeitung. Das wirkt am Anfang ungewohnt, weil du Methoden aufrufst und trotzdem noch kein Ergebnis siehst. Genau darin liegt aber ihr Nutzen. Kotlin kann Arbeit aufschieben, bündeln und bei Bedarf früher abbrechen.

Ein Beispiel: Du hast 10.000 Artikel und suchst die ersten 10 sichtbaren Artikel für einen Screen. Mit einer Liste können mehrere komplette Zwischenlisten entstehen, obwohl du am Ende nur 10 Elemente brauchst. Mit einer Sequence kann die Verarbeitung stoppen, sobald 10 passende Elemente gefunden sind. Das spart besonders dann Arbeit, wenn Filter und Mapping teuer sind oder wenn die passenden Elemente früh in der Datenquelle vorkommen.

Es gibt aber Grenzen. Manche Operationen brauchen trotzdem mehr Daten. sortedBy muss beispielsweise alle relevanten Elemente kennen, bevor eine sortierte Reihenfolge entsteht. Auch distinct muss bereits gesehene Werte merken. Eine Sequence macht solche Operationen nicht magisch billig. Sie hilft vor allem bei Ketten, die Element für Element verarbeitet werden können, und bei Endoperationen, die früh beenden dürfen.

Im Android-Alltag taucht das häufig in Mapper-Code auf. Ein Repository liefert Domain-Objekte. Ein ViewModel filtert sie nach Status, wandelt sie in UI-Modelle um und nimmt nur die Elemente, die für den aktuellen Screen relevant sind. In Jetpack Compose willst du solche Transformationen nicht unnötig bei jeder Recomposition neu berechnen. Deshalb gehört die Frage nach Sequences immer zur größeren Frage: Wo liegt die Berechnung, wie oft läuft sie, und wie groß ist die Datenmenge?

Sequences passen gut in ViewModels, Use Cases oder reine Mapper-Funktionen. Sie sind weniger passend direkt in Composables, wenn dadurch bei jeder Recomposition dieselbe Pipeline neu aufgebaut und ausgeführt wird. Compose-Code sollte UI beschreiben. Teurere Datenaufbereitung gehört meist vorher in den State, zum Beispiel in ein ViewModel. So bleibt die UI leichter testbar und die Performance besser kontrollierbar.

Auch für Tests sind Sequences angenehm, weil die Logik oft als reine Funktion formulierbar ist. Du kannst eine Liste von Eingaben übergeben und prüfen, ob die erwarteten UI-Modelle entstehen. Dabei testest du nicht die Kotlin-Standardbibliothek, sondern deine Entscheidungslogik: Welche Elemente werden gefiltert, wie wird gemappt, wann wird begrenzt?

In der Praxis

Stell dir vor, deine App zeigt Lernartikel in einer Liste. Aus einer lokalen Datenquelle bekommst du viele Article-Objekte. Für die Startseite brauchst du nur die ersten fünf veröffentlichten Artikel, die zum aktuellen Suchbegriff passen. Außerdem willst du sie in ein kleines UI-Modell umwandeln.

data class Article(
    val id: String,
    val title: String,
    val summary: String,
    val published: Boolean,
    val tags: List<String>
)

data class ArticleCardUi(
    val id: String,
    val title: String,
    val subtitle: String
)

fun buildArticleCards(
    articles: List<Article>,
    query: String
): List<ArticleCardUi> {
    val normalizedQuery = query.trim().lowercase()

    return articles
        .asSequence()
        .filter { it.published }
        .filter { article ->
            normalizedQuery.isBlank() ||
                article.title.lowercase().contains(normalizedQuery) ||
                article.tags.any { tag -> tag.lowercase().contains(normalizedQuery) }
        }
        .map { article ->
            ArticleCardUi(
                id = article.id,
                title = article.title,
                subtitle = article.summary.take(90)
            )
        }
        .take(5)
        .toList()
}

Die Pipeline liest sich von oben nach unten: erst veröffentlichte Artikel, dann Suchfilter, dann Mapping, dann Begrenzung. Durch asSequence() werden die Schritte verzögert. Die Pipeline wird erst bei toList() ausgeführt. Wichtig ist hier take(5): Sobald fünf passende UI-Modelle gefunden wurden, muss die Sequence keine weiteren Elemente mehr durch alle Schritte schicken.

Ohne Sequence wäre der Code oft ähnlich lesbar, aber bei großen Listen potenziell arbeitsintensiver. Eine normale Collection-Kette kann nach jedem Filter und nach dem Mapping neue Listen erzeugen. Bei zehn Artikeln ist das egal. Bei vielen tausend Einträgen, häufiger Ausführung und teureren Mappings kann es sichtbar werden, etwa durch mehr Speicherverbrauch oder kleine Verzögerungen beim Aktualisieren von UI-State.

Eine gute Entscheidungsregel lautet: Nutze Sequences, wenn mindestens zwei Bedingungen zusammenkommen. Erstens verarbeitest du eine größere Sammlung oder eine Kette mit mehreren Schritten. Zweitens kann die Pipeline früher stoppen oder du willst Zwischenlisten vermeiden. Wenn du nur drei Elemente mappst, bringt eine Sequence meist keinen Vorteil. Dann ist die normale Collection-API oft direkter und genauso gut.

Eine typische Stolperfalle ist die Annahme, dass eine Sequence immer schneller ist. Das stimmt nicht. Eine Sequence hat selbst einen gewissen Overhead, weil die Pipeline über Iteratoren arbeitet. Bei kleinen Listen kann eine normale Liste schneller oder zumindest gleich schnell sein. Performance-Verbesserung ist daher kein Glaubenssatz. Du prüfst sie mit realistischen Daten, Profiler, Benchmarks oder zumindest mit bewusst gesetzten Tests für die betroffene Funktion.

Eine zweite Stolperfalle ist eine fehlende Terminal-Operation. Dieser Code verarbeitet nichts:

val cards = articles
    .asSequence()
    .filter { it.published }
    .map { it.title }

cards ist hier nur eine Sequence. Erst cards.toList(), cards.firstOrNull() oder eine andere Terminal-Operation startet die Verarbeitung. Das kann in Debugging-Situationen verwirren. Wenn du Breakpoints in filter oder map setzt, werden sie erst erreicht, wenn du das Ergebnis wirklich abrufst.

Eine dritte Stolperfalle betrifft Seiteneffekte. Du solltest in filter und map keine wichtigen Zustandsänderungen verstecken. Weil die Ausführung verzögert ist, passiert der Seiteneffekt nicht beim Erstellen der Pipeline, sondern später. Außerdem kann er je nach Terminal-Operation nur für einen Teil der Elemente passieren. Wenn take(5) früh stoppt, werden Seiteneffekte für spätere Elemente nie ausgeführt. In professionellem Android-Code hältst du solche Pipelines deshalb möglichst rein: Eingaben hinein, Ergebnis heraus.

Für Compose ist zusätzlich wichtig: Baue teure Pipelines nicht unkontrolliert direkt im Composable, wenn sie bei jeder Recomposition erneut laufen könnten. Wenn die Datenaufbereitung fachliche Logik enthält, gehört sie meist in eine Funktion, einen Use Case oder das ViewModel. Das Composable bekommt dann bereits fertigen UI-State. Falls du lokal im Composable ableitest, solltest du bewusst mit stabilen Eingaben und geeigneten Compose-Mechanismen arbeiten. Der Kern bleibt aber derselbe: Eine Sequence löst nur die Verarbeitungskette, nicht automatisch das Lebenszyklus- oder State-Management.

Du kannst dein Verständnis gut mit einem kleinen Test absichern. Schreibe eine Funktion wie buildArticleCards, gib ihr eine Liste mit veröffentlichten und unveröffentlichten Artikeln, verschiedenen Tags und einem Suchbegriff. Prüfe dann, dass nur passende Artikel zurückkommen und höchstens fünf Elemente enthalten sind. Zusätzlich kannst du beim Debuggen in die Filter setzen und beobachten, dass die Pipeline erst bei toList() läuft. In Code-Reviews achtest du auf drei Fragen: Ist die Datenmenge groß genug, ist die Pipeline lang genug, und macht die Terminal-Operation den verzögerten Ablauf wirklich nützlich?

Fazit

Sequences helfen dir, Kotlin-Daten in Android gezielt verzögert zu verarbeiten. Sie sind besonders nützlich bei großen Collections, mehreren Transformationsschritten und Pipelines, die früh abbrechen können. Gleichzeitig sind sie kein Standardersatz für jede Liste und kein Ersatz für Flow, Paging oder saubere Architektur. Prüfe beim nächsten Mapper oder ViewModel bewusst, ob eine Collection-Kette unnötige Zwischenlisten erzeugt. Setze Breakpoints in filter und map, schreibe einen kleinen Test für das Ergebnis und achte im Code-Review darauf, ob die Sequence wirklich Arbeit spart oder nur zusätzliche Komplexität einführt.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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