Kotlin Performance Awareness
Du lernst, wie Kotlin-Entscheidungen Speicher und CPU in Android-Apps beeinflussen.
Kotlin ist auf Android die Standardsprache für moderne App-Entwicklung. Du bekommst eine klare Syntax, sichere Null-Behandlung, starke Collections und gute Integration mit Jetpack, Compose, Coroutines und Architekturkomponenten. Performance Awareness heißt dabei nicht, dass du jeden Ausdruck misstrauisch optimieren musst. Es heißt: Du entwickelst ein Gespür dafür, welche Kotlin-Entscheidungen auf einem mobilen Gerät Speicher belegen, CPU-Zeit kosten und dadurch UI-Flüssigkeit, Akkulaufzeit oder Startzeit beeinflussen.
Was ist das?
Kotlin Performance Awareness ist die Fähigkeit, Kotlin-Code nicht nur nach Lesbarkeit und Korrektheit zu beurteilen, sondern auch nach seinen Kosten zur Laufzeit. Besonders wichtig sind drei Begriffe: allocation, collections und inline.
Eine Allocation entsteht, wenn zur Laufzeit ein neues Objekt angelegt wird. Das kann ein Datenobjekt, eine Liste, ein Lambda, ein Iterator, ein Wrapper oder ein temporärer String sein. Auf Android ist das relevant, weil Geräte begrenzte Ressourcen haben und die UI in sehr kurzen Zeitfenstern reagieren muss. Wenn du in einem häufig aufgerufenen Codepfad viele Objekte erzeugst, muss der Garbage Collector später aufräumen. Dieses Aufräumen kann CPU-Zeit kosten und im ungünstigen Moment sichtbare Ruckler verursachen.
Collections sind in Kotlin sehr angenehm: map, filter, flatMap, groupBy und ähnliche Funktionen machen Code kurz und gut lesbar. Der Preis ist, dass viele dieser Operationen neue Collections oder Iteratoren erzeugen. Das ist in normalem App-Code oft völlig akzeptabel. In heißen Pfaden, etwa beim Rendern langer Listen, bei LazyColumn-Items, in Animationen, in Textverarbeitung großer Datenmengen oder bei häufiger State-Berechnung, können diese Zwischenobjekte aber auffallen.
inline ist ein Kotlin-Werkzeug, mit dem Funktionsaufrufe an der Aufrufstelle eingefügt werden können. Das ist besonders bei Funktionen mit Lambdas interessant, weil dadurch bestimmte Lambda-Objekte und Call-Overhead vermieden werden können. Viele Standardfunktionen aus Kotlin nutzen dieses Prinzip bereits. Für dich ist wichtig: inline ist kein allgemeiner Leistungs-Schalter, sondern ein gezieltes Werkzeug für kleine, häufig genutzte Funktionen mit Lambda-Parametern.
Der Anfängerfehler besteht darin, Performance entweder komplett zu ignorieren oder viel zu früh an jeder Stelle zu optimieren. Der bessere Mittelweg ist: Schreibe zuerst klaren Code, erkenne dann die Pfade, die häufig laufen oder große Datenmengen verarbeiten, und prüfe dort bewusst, welche Kotlin-Features Kosten erzeugen.
Wie funktioniert es?
Das wichtigste mentale Modell lautet: Kotlin-Code sieht oft kompakter aus als der Code, der zur Laufzeit ausgeführt wird. Der Compiler erzeugt JVM-Bytecode, und dieser Code läuft auf Android über die Android Runtime. Viele Kotlin-Sprachmittel sind effizient umgesetzt, aber sie sind nicht kostenlos. Du solltest deshalb lernen, zwischen selten ausgeführtem Steuerungscode und häufig ausgeführtem Arbeitscode zu unterscheiden.
Ein Beispiel: Wenn du beim Öffnen eines Einstellungsbildschirms einmal eine Liste filterst, ist eine gut lesbare Collection-Kette meist passend. Wenn du dieselbe Kette aber bei jeder Texteingabe, jeder Scrollbewegung oder jeder Compose-Recomposition ausführst, kann sie zu teuer werden. Compose macht diese Unterscheidung besonders sichtbar, weil Funktionen mehrfach aufgerufen werden können. Eine Composable-Funktion ist nicht automatisch ein einmaliger Aufbau wie eine klassische XML-View. Sie kann neu ausgewertet werden, wenn State sich ändert. Alles, was darin unnötig neu angelegt wird, kann sich summieren.
Allocation bedeutet dabei nicht nur data class(...). Auch scheinbar kleine Dinge können Objekte erzeugen: ein neues List-Ergebnis nach filter, ein neuer String durch Verkettung in einer Schleife, ein Lambda, das Werte aus dem äußeren Scope einfängt, oder ein Pair, das nur benutzt wird, um kurz zwei Werte weiterzureichen. Solche Objekte sind nicht verboten. Du solltest sie nur dort hinterfragen, wo sie oft entstehen.
Collections haben zwei Seiten. Sie verbessern Lesbarkeit und drücken Absicht klar aus. Gleichzeitig sind viele Operationen eager, also sofort ausführend. users.filter { it.active }.map { it.name } erzeugt erst eine gefilterte Liste und danach eine zweite Liste mit Namen. Bei zehn Einträgen ist das kaum relevant. Bei zehntausend Einträgen oder häufigen Wiederholungen wird es messbar. Kotlin bietet mit Sequence eine lazy Alternative, bei der Operationen verzögert und Element für Element verarbeitet werden. Aber auch Sequence ist nicht automatisch schneller. Für kleine Listen kann der zusätzliche Abstraktionsaufwand sogar unnötig sein. Die Regel lautet: Nutze normale Collections für Klarheit, prüfe bei großen Datenmengen oder langen Ketten, ob eine Schleife oder asSequence() sinnvoller ist.
Inline-Funktionen wirken auf einer anderen Ebene. Wenn du eine kleine Hilfsfunktion schreibst, die ein Lambda annimmt, kann der Compiler den Funktionskörper an der Aufrufstelle einsetzen. Dadurch kann er bestimmte Objekterzeugungen vermeiden und weitere Optimierungen ermöglichen. Kotlin-Standardfunktionen wie let, run, apply, also, use, forEach und viele Collection-Funktionen sind deshalb oft als inline implementiert. Das bedeutet aber nicht, dass du jede eigene Funktion mit inline markieren solltest. Inline vergrößert den generierten Code, weil der Funktionskörper an mehreren Stellen kopiert werden kann. Bei größeren Funktionen oder selten genutzten APIs kann das mehr schaden als helfen.
In Android-Projekten taucht dieses Thema an mehreren Stellen auf. In ViewModels berechnest du UI-State aus Datenströmen. In Repositories wandelst du Netzwerk- oder Datenbankmodelle um. In Compose leitest du aus State sichtbare Werte ab. In Tests vergleichst du Listen, Zustände und Ereignisse. An all diesen Stellen ist Klarheit wichtig. Performance Awareness ergänzt diese Klarheit um die Frage: Passiert diese Arbeit selten, oder passiert sie sehr oft?
Auch Berechtigungen und System-APIs haben einen Bezug. Wenn du etwa nach einer erteilten Berechtigung Standort-, Kamera- oder Mediendaten verarbeitest, kann plötzlich viel Datenmaterial in deine App kommen. Die Android-Dokumentation zu Berechtigungen erklärt, wie du Zugriff sauber anfragst. Performance Awareness beginnt danach: Du solltest große Ergebnisse nicht unnötig kopieren, mehrfach sortieren oder in jedem UI-Zyklus neu berechnen.
In der Praxis
Stell dir vor, du baust eine Kontaktliste in einer Android-App. Nutzer können suchen, nur Favoriten anzeigen und die Treffer alphabetisch sortieren. Für eine kleine Liste wirkt diese Lösung gut lesbar:
data class Contact(
val id: Long,
val name: String,
val isFavorite: Boolean
)
fun visibleContacts(
contacts: List<Contact>,
query: String,
favoritesOnly: Boolean
): List<Contact> {
return contacts
.filter { contact ->
!favoritesOnly || contact.isFavorite
}
.filter { contact ->
contact.name.contains(query, ignoreCase = true)
}
.sortedBy { contact ->
contact.name.lowercase()
}
}
Dieser Code ist korrekt, aber du solltest seine Kosten erkennen. Die erste filter-Operation erzeugt eine neue Liste. Die zweite filter-Operation erzeugt wieder eine neue Liste. sortedBy erzeugt ebenfalls ein sortiertes Ergebnis. Zusätzlich wird für jeden Vergleich oder Sortierschlüssel lowercase() aufgerufen, wodurch neue Strings entstehen können. Bei wenigen Kontakten ist das kein Problem. Bei vielen Kontakten, bei jeder Tastatureingabe und in direkter Verbindung mit Compose-State kann diese Arbeit spürbar werden.
Eine erste Verbesserung besteht nicht darin, alles komplizierter zu machen. Oft reicht es, Berechnung und UI sauber zu trennen. Leite die sichtbare Liste im ViewModel oder in einer stabilen State-Schicht ab, nicht mehrfach direkt in jeder Composable. In Compose kannst du außerdem remember verwenden, wenn eine Berechnung nur dann neu laufen soll, wenn sich ihre Eingaben ändern:
@Composable
fun ContactList(
contacts: List<Contact>,
query: String,
favoritesOnly: Boolean
) {
val visible = remember(contacts, query, favoritesOnly) {
contacts
.asSequence()
.filter { !favoritesOnly || it.isFavorite }
.filter { it.name.contains(query, ignoreCase = true) }
.sortedBy { it.name.lowercase() }
.toList()
}
LazyColumn {
items(
items = visible,
key = { contact -> contact.id }
) { contact ->
Text(text = contact.name)
}
}
}
Das ist nicht automatisch die beste Lösung für jede App, zeigt aber die Denkweise. remember verhindert unnötige Wiederholungen innerhalb der Composable, solange die Eingaben gleich bleiben. asSequence() vermeidet Zwischenlisten bei den Filter-Schritten. Am Ende brauchst du mit toList() trotzdem eine Liste, weil LazyColumn eine stabile Collection verarbeiten soll. Die Sortierung bleibt eine relevante Arbeit; sie kann nicht verschwinden, nur besser platziert werden.
Eine typische Stolperfalle ist, asSequence() reflexartig überall zu verwenden. Für kurze Listen und einfache Operationen sind normale Collections oft schneller, leichter zu lesen und leichter zu debuggen. Sequences lohnen sich eher, wenn du mehrere Transformationen auf größeren Datenmengen kombinierst oder früh abbrechen kannst, etwa mit firstOrNull. Performance Awareness bedeutet also nicht, eine Lieblings-API zu haben. Es bedeutet, die Kosten passend zum Kontext einzuschätzen.
Eine zweite Stolperfalle ist Arbeit in Composables, die bei jeder Recomposition neue Objekte erzeugt. Beispiel: Du baust direkt im Parameter eine formatierte Liste, erzeugst bei jedem Aufruf neue Lambdas mit eingefangenen Werten oder sortierst in jedem items-Block erneut. Compose ist effizient, aber es kann nur gut arbeiten, wenn dein Code stabile Eingaben und nachvollziehbare Zustände liefert. Berechnungen, die teuer oder datenabhängig sind, gehören meist in ein ViewModel, eine Use-Case-Funktion oder einen remember-Block mit korrekten Keys.
Für inline gilt eine andere Entscheidungsregel: Verwende es für kleine Hilfsfunktionen, die häufig aufgerufen werden und Lambda-Parameter haben, wenn du einen konkreten Nutzen erkennst. Ein Beispiel ist eine kleine Messfunktion während der Entwicklung:
inline fun <T> measureDebug(
label: String,
block: () -> T
): T {
val start = android.os.SystemClock.elapsedRealtimeNanos()
return try {
block()
} finally {
val elapsedMs =
(android.os.SystemClock.elapsedRealtimeNanos() - start) / 1_000_000.0
android.util.Log.d("Perf", "$label: $elapsedMs ms")
}
}
Diese Funktion ist klein, nimmt ein Lambda und kann in Debug-Code nützlich sein. Durch inline kann der Lambda-Aufruf günstiger werden. In produktivem Code solltest du solche Messungen kontrolliert einsetzen und nicht unüberlegt überall Log-Ausgaben erzeugen. Logs selbst kosten ebenfalls Zeit und können sensible Informationen enthalten.
Wenn du eigene APIs schreibst, prüfe bei inline immer drei Fragen. Erstens: Ist die Funktion klein genug, damit Code-Duplizierung kein Problem wird? Zweitens: Wird sie häufig genug genutzt, damit der Nutzen relevant ist? Drittens: Nimmt sie Lambdas oder reified generics, die von inline tatsächlich profitieren? Wenn du diese Fragen nicht klar beantworten kannst, ist eine normale Funktion meist die bessere Wahl.
Im Alltag hilft dir eine einfache Reihenfolge. Schreibe zuerst lesbaren Kotlin-Code. Markiere dann die Stellen, die oft laufen: Listenfilter bei Suche, Mapper in großen Datenströmen, Compose-Berechnungen, Schleifen in Parsern, Sortierungen, Bild- oder Mediendatenverarbeitung. Prüfe dort Allocations und Collection-Ketten. Miss danach, statt nur zu raten. Android Studio Profiler, einfache Zeitmessungen im Debug-Build, Unit-Tests für Mapper und Code-Reviews sind praktische Werkzeuge. In einem Review kannst du gezielt fragen: Wird hier eine neue Liste erzeugt? Läuft diese Berechnung bei jeder Recomposition? Ist diese String-Operation in einer Schleife? Brauchen wir inline, oder macht es den Code nur schwerer zu ändern?
Wichtig ist auch, Performance nicht gegen Lesbarkeit auszuspielen. Eine handgeschriebene Schleife kann schneller sein, aber sie kann auch Fehler einführen. Wenn du von einer Collection-Kette auf eine Schleife wechselst, solltest du Tests haben, die das Verhalten absichern. Beispiel: Filterlogik, Groß-/Kleinschreibung, Sortierreihenfolge und leere Eingaben sollten durch Unit-Tests abgedeckt sein. So lernst du, Optimierungen kontrolliert vorzunehmen.
Fazit
Kotlin Performance Awareness ist ein praktisches Qualitätsmerkmal auf dem Weg vom Anfänger zum professionellen Android-Entwickler. Du musst nicht jede Allocation vermeiden und nicht jede Collection-Kette ersetzen. Du solltest aber erkennen, wo Kotlin-Code zur Laufzeit Objekte erzeugt, wo Collections Zwischenlisten bauen und wo inline wirklich helfen kann. Nimm dir eine vorhandene Listen- oder State-Berechnung aus deinem Projekt, schreibe einen kleinen Test dafür, prüfe sie im Debugger oder Profiler und bespreche im Code-Review bewusst die Kosten. So trainierst du ein Gespür, das deine Apps stabiler, flüssiger und besser wartbar macht.