Sortierung in Android-Apps
Sortierung bestimmt, welche Daten Nutzer zuerst sehen. Du lernst, wie Ordnung, Comparator und Stabilität Android-Listen verlässlich machen.
Sortierung wirkt auf den ersten Blick wie ein kleines Detail: eine Liste steht eben in einer bestimmten Reihenfolge. In echten Android-Apps entscheidet diese Reihenfolge aber darüber, welche Aufgabe Nutzer zuerst sehen, welches Suchergebnis wichtig wirkt und ob eine Oberfläche ruhig oder sprunghaft erscheint.
Was ist das?
Sorting bedeutet, Elemente nach einer definierten Ordnung anzuordnen. Diese Ordnung kann technisch sein, zum Beispiel alphabetisch nach Namen oder absteigend nach Zeitstempel. Sie kann aber auch fachlich sein: offene Aufgaben vor erledigten Aufgaben, lokale Treffer vor entfernten Treffern, favorisierte Kontakte vor allen anderen oder bezahlte Rechnungen nach unbezahlten Rechnungen.
Für dich als Android-Entwickler ist wichtig: Sortierung ist nicht nur eine Methode auf einer Liste. Sie ist eine Entscheidung über Bedeutung. Wenn eine App eine LazyColumn mit Nachrichten zeigt, dann entsteht durch die Reihenfolge ein mentales Modell. Nutzer erwarten, dass neue Nachrichten oben stehen, dass ältere beim Scrollen folgen und dass sich die Liste nicht ohne nachvollziehbaren Grund neu mischt. Wenn diese Erwartung bricht, fühlt sich die App unzuverlässig an, auch wenn die Daten korrekt geladen wurden.
Im Android-Kontext taucht Sorting an vielen Stellen auf. Du sortierst Daten aus einer Datenbank, Ergebnisse aus einem Repository, Zustände in einem ViewModel oder Einträge direkt vor der Darstellung in Compose. Je näher die Sortierung an der UI liegt, desto stärker solltest du prüfen, ob sie wirklich nur Darstellung betrifft. Wenn die Reihenfolge eine Geschäftsregel ausdrückt, gehört sie eher in die Domain- oder Data-Schicht. Wenn sie eine reine Anzeigevariante ist, etwa „Sortieren nach Name“ in einer Liste, kann das ViewModel die Auswahl des Nutzers in einen passenden Comparator übersetzen.
Drei Begriffe solltest du sauber trennen. Ordering ist die gewünschte Ordnung: Welche Elemente kommen vor anderen? Ein Comparator ist die konkrete Vergleichslogik, die zwei Elemente bewertet. Stability beschreibt, ob Elemente mit gleichem Sortierwert ihre vorherige relative Reihenfolge behalten. Diese Stabilität ist besonders wichtig, wenn du mehrere Sortierkriterien nacheinander anwendest oder wenn eine UI-Liste während Updates nicht springen soll.
Ein Beispiel: Du hast Aufgaben mit Priorität und Erstellungsdatum. Wenn zwei Aufgaben dieselbe Priorität haben, möchtest du vermutlich die ältere oder neuere zuerst zeigen. Wenn du diese zweite Regel vergisst, hängt die Reihenfolge eventuell von der Datenquelle ab. Heute wirkt die Liste korrekt, morgen nach einem Sync steht etwas anders. Genau solche Fehler sind schwer zu finden, weil sie oft nicht wie ein klassischer Crash aussehen, sondern wie ein unruhiges Produktverhalten.
Wie funktioniert es?
In Kotlin arbeitest du meistens mit sortedBy, sortedByDescending, sortedWith und compareBy. Diese Funktionen erzeugen neue sortierte Listen. Das passt gut zu moderner Android-Architektur, weil UI-State möglichst unveränderlich weitergegeben wird. Ein ViewModel kann also aus einem Flow von Daten einen neuen UI-State ableiten, ohne die ursprüngliche Liste zu verändern. Für Compose ist das hilfreich, weil nachvollziehbare Zustandsänderungen leichter zu verstehen und zu testen sind.
Das mentale Modell ist einfach, aber streng: Eine Sortierung braucht eine Vergleichsregel, und diese Regel muss konsistent sein. Wenn A vor B kommt und B vor C, dann sollte auch A vor C kommen. Wenn zwei Elemente gleichwertig sind, sollte klar sein, ob ein weiteres Kriterium greift. Fehlt diese Konsistenz, entstehen Listen, die bei kleinen Datenänderungen anders aussehen, als du erwartest.
Ein Comparator beantwortet nicht die Frage „Ist dieses Element wichtig?“, sondern „Wie stehen zwei konkrete Elemente zueinander?“. In Kotlin sieht das oft so aus: compareBy<Task> { it.isDone }.thenByDescending { it.priority }.thenBy { it.createdAt }. Die Reihenfolge der Kriterien ist dabei entscheidend. Das erste Kriterium ist das stärkste. Weitere Kriterien greifen nur, wenn vorherige Kriterien gleich sind.
In Android-Projekten solltest du außerdem unterscheiden, wo sortiert wird. Eine Room-Query mit ORDER BY ist sinnvoll, wenn die Datenmenge groß ist oder die Sortierung direkt zur Abfrage gehört. Das Repository oder ein Use Case ist passend, wenn die App eine fachliche Rangfolge bildet, etwa „dringende Aufgaben zuerst“. Das ViewModel ist passend, wenn die UI zwischen mehreren Ansichten umschaltet, etwa Name, Datum oder Status. Direkt in einer Composable solltest du nur mit Vorsicht sortieren. Wenn bei jeder Recomposition eine größere Liste neu sortiert wird, kann das unnötige Arbeit erzeugen. Die Compose-Performance-Dokumentation betont allgemein, dass du teure Arbeit aus häufig neu ausgeführten UI-Pfaden heraushalten solltest. Sortierung ist ein typisches Beispiel dafür.
Stability verdient besondere Aufmerksamkeit. Angenommen, du sortierst zuerst nach Datum und danach nach Status. Wenn die zweite Sortierung stabil ist, behalten Elemente mit gleichem Status ihre vorherige Datumsordnung. Wenn sie nicht stabil wäre, könnte diese Ordnung verloren gehen. In Kotlin sind die üblichen Sortierfunktionen für Listen stabil. Trotzdem solltest du dich nicht blind darauf verlassen, dass eine zufällige vorherige Ordnung fachlich ausreicht. Schreibe lieber alle relevanten Kriterien aus. Das macht den Code lesbar und verhindert, dass eine Datenquelle unbemerkt deine UI-Reihenfolge bestimmt.
Für Compose kommt ein weiterer Punkt hinzu: stabile Identitäten. Wenn du eine sortierte Liste in einer LazyColumn darstellst, solltest du stabile Keys verwenden, zum Beispiel eine Datenbank-ID. Sonst kann Compose bei Umordnungen Elemente schlechter zuordnen. Das betrifft nicht die Sortierung selbst, aber die Wirkung der Sortierung auf der Oberfläche. Ohne Keys können Zustände in Listeneinträgen an der falschen Stelle erscheinen, etwa ein aufgeklappter Eintrag oder ein Textfeldzustand.
Eine gute Sortierregel ist also explizit, stabil im Verhalten, an der richtigen Schicht platziert und durch Tests abgesichert. Das klingt nach viel für eine Liste, ist aber normale Alltagsarbeit in Android-Apps. Sobald Daten aus Netzwerk, Datenbank und Nutzereingaben zusammenkommen, ist die Reihenfolge kein Nebenthema mehr.
In der Praxis
Stell dir eine Aufgaben-App vor. Jede Aufgabe hat einen Titel, eine Priorität, einen Status und einen Zeitpunkt. Die Produktregel lautet: unerledigte Aufgaben zuerst, dann hohe Priorität, dann ältere Aufgaben vor neueren. Erledigte Aufgaben sollen nach unten wandern, aber innerhalb ihrer Gruppe weiterhin nachvollziehbar sortiert bleiben.
Eine mögliche Modellierung sieht so aus:
data class Task(
val id: String,
val title: String,
val priority: Int,
val isDone: Boolean,
val createdAtMillis: Long
)
private val taskComparator =
compareBy<Task> { it.isDone }
.thenByDescending { it.priority }
.thenBy { it.createdAtMillis }
.thenBy { it.id }
fun List<Task>.toSortedUiList(): List<Task> =
sortedWith(taskComparator)
Diese Sortierung nutzt mehrere Kriterien. isDone steht zuerst, weil false vor true kommt. Dadurch erscheinen unerledigte Aufgaben vor erledigten. Danach folgt die Priorität absteigend. Anschließend entscheidet das Erstellungsdatum. Die ID am Ende wirkt als feste Zusatzregel, falls zwei Aufgaben in allen anderen Werten gleich sind. Das ist keine fachlich besonders spannende Regel, aber sie macht die Ausgabe deterministisch. Deterministisch heißt: Für dieselben Eingaben bekommst du dieselbe Reihenfolge.
Im ViewModel könntest du diese Logik anwenden, bevor der UI-State an Compose geht:
class TaskViewModel(
private val repository: TaskRepository
) : ViewModel() {
val uiState: StateFlow<TaskUiState> =
repository.observeTasks()
.map { tasks ->
TaskUiState(
tasks = tasks.toSortedUiList()
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskUiState()
)
}
data class TaskUiState(
val tasks: List<Task> = emptyList()
)
In Compose nutzt du dann die sortierte Liste, ohne sie in der Composable erneut zu sortieren:
@Composable
fun TaskListScreen(
state: TaskUiState,
onTaskClick: (String) -> Unit
) {
LazyColumn {
items(
items = state.tasks,
key = { task -> task.id }
) { task ->
TaskRow(
task = task,
onClick = { onTaskClick(task.id) }
)
}
}
}
Diese Aufteilung ist bewusst gewählt. Das Repository liefert Daten. Das ViewModel formt daraus UI-State. Die Composable stellt dar. Die Sortierung ist dadurch testbar, und die UI bleibt frei von unnötiger Berechnungsarbeit. Wenn die Sortierregel später durch eine Nutzerauswahl steuerbar wird, kannst du im ViewModel abhängig von SortMode einen anderen Comparator wählen.
Eine typische Stolperfalle ist das Sortieren direkt im Compose-Body:
@Composable
fun TaskListScreen(tasks: List<Task>) {
val sortedTasks = tasks.sortedWith(taskComparator)
LazyColumn {
items(sortedTasks) { task ->
TaskRow(task = task)
}
}
}
Bei kleinen Listen fällt das kaum auf. Bei größeren Listen, häufigen State-Updates oder komplexeren Vergleichsregeln kann es aber unnötig teuer werden. Außerdem versteckst du fachliche Logik in der Darstellung. Besser ist, die sortierte Liste als Teil des UI-State zu liefern oder, wenn die Sortierung wirklich nur lokal zur UI gehört, sie mit remember an die Eingabe zu binden:
val sortedTasks = remember(tasks) {
tasks.sortedWith(taskComparator)
}
Auch das ist nur dann passend, wenn die Composable die richtige Stelle für diese Entscheidung ist. Für Lernprojekte ist remember hilfreich, um den Zusammenhang zwischen Recomposition und Berechnung zu sehen. Für produktive Architektur solltest du trotzdem fragen: Ist das eine Darstellungsentscheidung oder eine fachliche Rangfolge?
Eine zweite Stolperfalle ist uneindeutige Sortierung. Viele Anfänger schreiben sortedBy { it.priority } und wundern sich, warum Aufgaben mit gleicher Priorität manchmal anders stehen. Technisch ist die Sortierung stabil, aber die Eingabeliste kann aus einer Datenbank, einem Netzwerk-Response oder einem zusammengeführten Cache kommen. Wenn diese Eingabeliste nicht selbst eine garantierte Ordnung hat, ist das Ergebnis aus Nutzersicht wacklig. Ergänze deshalb immer ein zweites oder drittes Kriterium, wenn gleiche Werte realistisch sind.
Eine dritte Stolperfalle betrifft Textsortierung. sortedBy { it.title } sortiert nach der natürlichen String-Ordnung. Das reicht oft für technische Namen, ist aber nicht immer passend für sichtbare, lokalisierte Inhalte. Groß- und Kleinschreibung, Umlaute und Spracheinstellungen können Erwartungen verändern. Wenn die App eine wichtige alphabetische Nutzerliste zeigt, solltest du prüfen, ob eine locale-bewusste Sortierung nötig ist. Das ist kein Grund, jede Liste kompliziert zu machen. Es ist ein Hinweis, dass Sortierung auch Teil der Produktqualität ist.
Tests machen Sortierlogik deutlich robuster. Ein Unit-Test braucht keine Android-UI. Du gibst eine unsortierte Liste hinein und prüfst die IDs in der erwarteten Reihenfolge:
@Test
fun sortsOpenHighPriorityOldTasksFirst() {
val tasks = listOf(
Task("a", "Done", priority = 10, isDone = true, createdAtMillis = 1),
Task("b", "Low", priority = 1, isDone = false, createdAtMillis = 3),
Task("c", "High old", priority = 10, isDone = false, createdAtMillis = 1),
Task("d", "High new", priority = 10, isDone = false, createdAtMillis = 2)
)
val result = tasks.toSortedUiList().map { it.id }
assertEquals(listOf("c", "d", "b", "a"), result)
}
Dieser Test prüft nicht Kotlin selbst. Er prüft deine Fachregel. Genau das ist der Nutzen. Wenn später jemand die Sortierung ändert, sieht die Code-Review sofort, ob die gewünschte Rangfolge noch gilt. In einer Continuous-Integration-Pipeline laufen solche Tests bei jedem Pull Request mit und verhindern, dass scheinbar kleine Änderungen an einer Liste die App-Qualität verschlechtern.
Für die Praxis kannst du dir eine Entscheidungsregel merken: Sortiere dort, wo die Ordnung fachlich entsteht, und gib der UI bereits eine klare, stabile Liste. Wenn die Sortierung durch den Nutzer ausgewählt wird, halte den Sortiermodus im UI-State und mappe ihn auf explizite Comparatoren. Wenn die Datenmenge groß ist, prüfe, ob die Datenbank sortieren sollte. Wenn die Liste in Compose dargestellt wird, verwende stabile Keys. Und wenn du nicht in einem Satz erklären kannst, warum Element A vor Element B steht, ist die Sortierregel noch nicht klar genug.
Fazit
Sortierung ist ein kleines Werkzeug mit großer Wirkung auf Wahrnehmung, Architektur und Qualität deiner Android-App. Du solltest nicht nur wissen, welche Kotlin-Funktion eine Liste ordnet, sondern auch verstehen, welche fachliche Aussage diese Ordnung trifft. Übe das mit einer eigenen Liste aus realistischen Daten: Schreibe einen Comparator mit mindestens drei Kriterien, zeige die Liste in Compose mit stabilen Keys, prüfe das Ergebnis im Debugger und sichere die Regel mit einem Unit-Test ab. So erkennst du früh, ob deine App nur Daten anzeigt oder ob sie den Nutzern eine verlässliche Reihenfolge bietet.