Collection Aggregation in Kotlin
Du lernst, Collections mit count, sum, fold und reduce auszuwerten. Der Artikel zeigt klare Regeln für lesbaren Kotlin-Code.
Wenn du in einer Android-App Listen verarbeitest, willst du oft nicht jedes Element einzeln anzeigen, sondern eine Zusammenfassung bilden: Wie viele Aufgaben sind offen? Wie hoch ist der Warenkorb? Welcher Status ergibt sich aus mehreren Einträgen? Genau dafür nutzt du Collection Aggregation in Kotlin.
Was ist das?
Collection Aggregation bedeutet: Du nimmst mehrere Werte aus einer Collection und berechnest daraus einen einzelnen Ergebniswert. Diese Collection kann zum Beispiel eine List, ein Set oder eine andere Kotlin-Sammlung sein. Das Ergebnis ist oft eine Zahl, ein Boolean, ein String oder ein eigenes Objekt.
Die wichtigsten Operationen in diesem Roadmap-Schritt sind count, sum, fold und reduce. Sie lösen ähnliche, aber nicht gleiche Aufgaben. count zählt Elemente. sum oder in modernem Kotlin häufig sumOf addiert Werte. fold baut aus einer Collection Schritt für Schritt ein Ergebnis auf und startet mit einem expliziten Anfangswert. reduce baut ebenfalls ein Ergebnis auf, verwendet aber das erste Element als Startwert.
Das mentale Modell ist einfach genug, aber wichtig: Eine Aggregation läuft gedanklich über alle Elemente und trägt ein Zwischenergebnis weiter. Am Ende bleibt ein Wert übrig. Du solltest also nicht zuerst an Schleifen denken, sondern an die Frage: “Welche Zusammenfassung brauche ich?” Wenn diese Frage klar ist, wird der passende Operator meistens deutlich.
Im Android-Alltag taucht das ständig auf. Ein ViewModel berechnet aus einer Liste von Domain-Objekten einen UI-State. Eine Compose-Oberfläche zeigt eine Badge-Zahl. Eine Checkout-Ansicht zeigt eine Gesamtsumme. Ein Test prüft, ob eine Liste genau drei fehlerhafte Einträge enthält. Aggregationen helfen dir, solche Absichten direkt im Code auszudrücken.
Der Vorteil ist nicht nur weniger Code. Der größere Vorteil ist Lesbarkeit. Eine Schleife mit mehreren veränderlichen Variablen kann korrekt sein, aber sie zwingt den Leser, die Absicht aus den einzelnen Schritten abzuleiten. items.count { !it.done } sagt dagegen sofort, was berechnet wird. Diese Klarheit ist besonders wichtig, wenn du in Teams arbeitest oder später Code im Code-Review erklären musst.
Wie funktioniert es?
count zählt die Anzahl der Elemente. Ohne Bedingung liefert es die Größe der Collection. Mit Predicate zählt es nur passende Elemente. Das Predicate ist eine Lambda-Funktion, die für jedes Element true oder false liefert. Dadurch eignet sich count sehr gut für Statusanzeigen, Filterstatistiken und Validierungen.
sum ist für einfache numerische Collections gedacht. In vielen Android-Projekten wirst du aber öfter sumOf sehen, weil du meist eine Eigenschaft aus einem Objekt addierst. Beispiel: Du hast eine Liste von Warenkorbpositionen und willst priceCents * quantity addieren. Dann ist sumOf klarer als vorheriges Mapping plus Summe.
fold ist der allgemeinere Aggregationsoperator. Er bekommt einen Startwert und eine Operation. Die Operation erhält immer zwei Werte: den aktuellen Akkumulator und das nächste Element. Der Akkumulator ist dein bisheriges Ergebnis. Nach jedem Element entsteht ein neuer Akkumulator. Am Ende gibt fold diesen letzten Wert zurück.
reduce sieht ähnlich aus, hat aber keinen separaten Startwert. Kotlin verwendet das erste Element der Collection als Anfang des Akkumulators. Das ist kompakt, wenn die Collection sicher nicht leer ist und Ergebnis- sowie Elementtyp zusammenpassen. Genau dort liegt aber auch die typische Stolperfalle: Auf einer leeren Collection wirft reduce eine Exception. In App-Code ist das oft riskant, weil Daten aus Netzwerk, Datenbank oder Benutzerfilterung leer sein können.
Der Unterschied zwischen fold und reduce ist daher keine reine Stilfrage. fold ist meistens die robustere Wahl, wenn du defensiv und gut testbar arbeiten willst. reduce passt eher, wenn eine fachliche Regel garantiert, dass mindestens ein Element vorhanden ist, und diese Regel im Code sichtbar ist.
Noch ein wichtiger Punkt: Aggregation sollte nicht mit Seiteneffekten vermischt werden. Ein fold, der nebenbei Logs schreibt, Mutable State verändert oder Repository-Aufrufe startet, wird schwer verständlich. Aggregationsfunktionen sind am stärksten, wenn sie aus Eingabedaten ein Ergebnis berechnen. Für Android-Architektur passt das gut: Reine Berechnungen im ViewModel oder in Use-Cases lassen sich leicht testen und stabil in Compose darstellen.
In Compose selbst solltest du Aggregationen bewusst platzieren. Wenn eine Liste bei jeder Recomposition erneut teuer ausgewertet wird, kann das unnötige Arbeit erzeugen. Für kleine Listen ist das meist kein Problem. Bei größeren Datenmengen oder häufigen Updates gehört die Berechnung eher in den UI-State des ViewModel oder in eine abgeleitete, klar begrenzte Berechnung. Die Grundregel bleibt: Erst Lesbarkeit, dann gezielte Optimierung, wenn ein echtes Problem sichtbar wird.
In der Praxis
Stell dir eine Aufgaben-App vor. Du hast eine Liste von Aufgaben und willst in deinem ViewModel einen kompakten Status für die UI berechnen: Anzahl offener Aufgaben, Anzahl erledigter Aufgaben und Fortschritt in Prozent. Dafür brauchst du keine manuelle Schleife.
data class Task(
val id: String,
val title: String,
val done: Boolean,
val estimateMinutes: Int
)
data class TaskSummary(
val openCount: Int,
val doneCount: Int,
val totalEstimateMinutes: Int,
val progressPercent: Int
)
fun List<Task>.toSummary(): TaskSummary {
val openCount = count { !it.done }
val doneCount = count { it.done }
val totalEstimateMinutes = sumOf { it.estimateMinutes }
val progressPercent = if (isEmpty()) {
0
} else {
(doneCount * 100) / size
}
return TaskSummary(
openCount = openCount,
doneCount = doneCount,
totalEstimateMinutes = totalEstimateMinutes,
progressPercent = progressPercent
)
}
Der Code ist kurz, aber nicht nur deshalb gut. Jede Zeile benennt die fachliche Absicht. openCount zählt offene Aufgaben. doneCount zählt erledigte Aufgaben. totalEstimateMinutes summiert Schätzungen. Die leere Liste wird bewusst behandelt, damit keine Division durch null entsteht.
Du könntest die offenen und erledigten Aufgaben auch in einer einzigen fold-Operation berechnen. Das ist nützlich, wenn du mehrere Werte in einem Durchlauf sammeln willst. Trotzdem solltest du abwägen, ob der Code dadurch lesbarer wird.
data class Counts(
val open: Int,
val done: Int
)
fun List<Task>.countStates(): Counts {
return fold(Counts(open = 0, done = 0)) { acc, task ->
if (task.done) {
acc.copy(done = acc.done + 1)
} else {
acc.copy(open = acc.open + 1)
}
}
}
Dieses Beispiel zeigt gut, wie fold arbeitet. Der Startwert ist Counts(0, 0). Für jede Aufgabe wird ein neuer Counts-Wert erzeugt. Das ist funktional sauber und vermeidet veränderlichen Zwischenzustand. Für Lernende ist aber wichtig: Nutze fold nicht nur, weil es fortgeschritten wirkt. Wenn zwei count-Aufrufe die Absicht klarer zeigen und die Liste klein ist, ist diese Variante oft besser.
Eine typische Fehlentscheidung ist ein zu cleveres reduce. Beispiel: Du willst alle Titel zu einem Text verbinden und schreibst tasks.map { it.title }.reduce { a, b -> "$a, $b" }. Das funktioniert nur, solange die Liste nicht leer ist. Sobald ein Filter keine Aufgaben liefert, kann die App abstürzen. Besser ist hier ein spezieller Operator wie joinToString, weil er die Absicht noch genauer ausdrückt. Falls du wirklich eine eigene Aggregation brauchst, nimm fold mit einem Startwert.
Eine gute Entscheidungsregel lautet: Verwende den spezifischsten gut lesbaren Operator. Für Anzahl nimm count. Für Summen nimm sumOf. Für selbst gebaute Ergebnisse mit sicherem Startwert nimm fold. Verwende reduce nur, wenn eine leere Collection fachlich ausgeschlossen ist und diese Annahme nachvollziehbar bleibt.
Auch Tests profitieren davon. Du kannst eine Aggregationsfunktion wie toSummary() mit wenigen Eingaben prüfen: leere Liste, nur offene Aufgaben, nur erledigte Aufgaben und gemischte Aufgaben. Diese Tests sind klein, aber wertvoll, weil sie fachliche Regeln absichern. Im Debugger kannst du außerdem einen Breakpoint in der Lambda von fold setzen und beobachten, wie sich der Akkumulator verändert. Dadurch verstehst du den Ablauf besser als durch reines Lesen.
Achte außerdem auf Zahlenbereiche. In Android-Apps werden Geldbeträge oft nicht als Double, sondern als Cent-Beträge in Long oder Int modelliert, je nach Domäne. Wenn du Preise summierst, sollte der Typ bewusst gewählt sein. sumOf übernimmt den Rückgabetyp deiner Lambda. Das ist praktisch, ersetzt aber keine fachliche Modellierung. Für reale Zahlungslogik brauchst du klare Regeln zu Rundung und Währung, auch wenn die Aggregation selbst nur eine Zeile ist.
Fazit
Collection Aggregation ist ein Kernwerkzeug für sauberen Kotlin-Code in Android-Projekten. Du verdichtest Listen zu aussagekräftigen Ergebnissen und machst fachliche Absichten direkt sichtbar. Prüfe beim nächsten eigenen Screen bewusst, wo du noch manuelle Schleifen für Anzahl, Summe oder Statusberechnung verwendest. Ersetze eine davon durch count, sumOf oder fold, schreibe zwei kleine Tests für leere und gemischte Daten, und lass im Code-Review besonders auf Lesbarkeit und leere Collections achten.