Not-Null Assertion in Kotlin vermeiden
`!!` kann Null-Sicherheit aushebeln. Du lernst, wie du Crash-Risiken in Android-Code gezielt reduzierst.
Kotlin gibt dir mit Nullable-Typen ein starkes Werkzeug gegen NullPointerException. Genau dieses Werkzeug hebelst du mit !! aus: Du sagst dem Compiler, dass ein Wert garantiert nicht null ist. Wenn du dich irrst, endet dein Code zur Laufzeit mit einem Crash. In Android-Apps ist das besonders riskant, weil viele Werte aus Lebenszyklus, Navigation, Netzwerk, Datenbank, Intent-Extras oder UI-Zustand kommen und nicht immer so stabil sind, wie sie im ersten Moment wirken.
Was ist das?
Not-Null Assertion vermeiden bedeutet: Du behandelst !! nicht als bequeme Abkürzung, sondern als Warnsignal. Der Operator !! wandelt in Kotlin einen nullable Wert wie String? in einen non-null Wert wie String um. Dabei findet keine echte Prüfung statt, die deinen Code sicherer macht. Kotlin erzeugt nur eine Laufzeitprüfung. Ist der Wert null, wirft die App eine Exception.
Das mentale Modell ist wichtig: Ein ? im Typ ist keine Störung, sondern Information. User? sagt dir: Dieser Wert kann fehlen. Das kann fachlich korrekt sein, etwa wenn noch kein Nutzer geladen wurde. Es kann technisch bedingt sein, etwa wenn ein Fragment seine View verloren hat. Oder es kann von außen kommen, etwa aus JSON, SharedPreferences oder einem Intent. Sobald du !! nutzt, ignorierst du diese Information.
In modernem Android-Code arbeitest du oft mit Kotlin, Jetpack Compose, ViewModels, Flow, Repositorys und asynchronen Daten. Dabei ist Null-Sicherheit kein Detail, sondern Teil der Architekturqualität. Ein unnötiges !! kann einen Crash in einer seltenen Reihenfolge auslösen: Bildschirm drehen, Request läuft noch, Navigation zurück, State wird später aktualisiert. Solche Fehler sind schwerer zu finden als offensichtliche Compilerfehler.
Darum gilt eine klare Regel: !! ist eine letzte Option, wenn du eine Invariante nachweisbar kontrollierst und keine bessere Modellierung möglich ist. In Lernprojekten solltest du fast immer nach einer Alternative suchen. In professionellem Code sollte jede Not-Null Assertion im Review auffallen und begründet werden.
Wie funktioniert es?
Kotlin unterscheidet zwischen Typen, die null sein dürfen, und Typen, die nicht null sein dürfen. String darf nicht null sein. String? darf null sein. Wenn du auf einem nullable Wert eine Methode aufrufen willst, verlangt der Compiler eine Entscheidung. Genau dort lernst du, bewusst mit fehlenden Werten umzugehen.
Du hast mehrere Werkzeuge. Mit einem Safe Call wie user?.name wird der Zugriff nur ausgeführt, wenn user nicht null ist. Mit dem Elvis-Operator ?: definierst du einen Ersatz oder verlässt die Funktion. Mit let kannst du nur im nicht-null Fall weiterarbeiten. Mit einer frühen Rückgabe hältst du den restlichen Code sauber. Mit einem passenden State-Modell kannst du sogar vermeiden, dass ein fachlich notwendiger Wert überhaupt nullable ist.
!! macht etwas anderes. Es sagt: “Ich übernehme die Verantwortung, dieser Wert ist nicht null.” Der Compiler glaubt dir. Die App zahlt den Preis, wenn deine Annahme falsch war. Deshalb ist !! nicht dasselbe wie eine saubere Prüfung. Es verschiebt ein Problem vom Compiler in die Laufzeit.
Im Android-Alltag taucht das an typischen Stellen auf. Ein Bundle kann ein Argument nicht enthalten. Ein API-Feld kann fehlen. Eine Datenbankabfrage kann keinen Treffer liefern. Ein MutableStateFlow<User?> kann beim Start noch null sein. Ein Compose-Screen kann Daten anzeigen wollen, bevor sie geladen sind. Ein Test kann versehentlich nur den Erfolgsfall abdecken. Wenn du dann user!!.name schreibst, baust du eine Crash-Stelle ein, die bei dir lokal vielleicht nie ausgelöst wird, bei Nutzern aber schon.
Bei Jetpack Compose solltest du nullable UI-State nicht durch !! erzwingen. Compose rendert oft mehrfach, und Zwischenzustände sind normal. Wenn ein Profil noch lädt, ist profile vielleicht null. Dein UI sollte diesen Zustand ausdrücken: Loading, Empty, Error oder Content. Das ist stabiler als ein erzwungener Zugriff.
Auch in ViewModels ist !! meist ein Zeichen für ein ungenaues Modell. Wenn ein Wert zwingend vorhanden sein muss, kann er als Constructor-Parameter, als non-null State oder als eigener State-Typ modelliert werden. Wenn er fehlen kann, sollte dein Code diesen Fall sichtbar behandeln. Diese Entscheidung ist wichtiger als der Operator selbst.
In der Praxis
Stell dir vor, du baust einen Profilbildschirm. Das Repository liefert den aktuellen Nutzer asynchron. Am Anfang gibt es noch keinen geladenen Nutzer. Eine schnelle, aber riskante Variante sieht so aus:
data class User(
val id: String,
val displayName: String
)
data class ProfileUiState(
val user: User? = null,
val isLoading: Boolean = true
)
@Composable
fun ProfileScreen(state: ProfileUiState) {
Text(text = state.user!!.displayName)
}
Dieser Code kompiliert. Er ist trotzdem fragil. Beim ersten Rendern ist user noch null, weil isLoading auf true steht. Der Screen crasht, bevor du einen Ladezustand anzeigen kannst. Das Problem liegt nicht in Compose, sondern in deiner Annahme.
Eine bessere Variante behandelt die möglichen Zustände sichtbar:
sealed interface ProfileUiState {
data object Loading : ProfileUiState
data class Content(val user: User) : ProfileUiState
data class Error(val message: String) : ProfileUiState
}
@Composable
fun ProfileScreen(state: ProfileUiState) {
when (state) {
ProfileUiState.Loading -> {
Text(text = "Profil wird geladen")
}
is ProfileUiState.Content -> {
Text(text = state.user.displayName)
}
is ProfileUiState.Error -> {
Text(text = state.message)
}
}
}
Hier ist user im Content-Zweig nicht nullable. Der Typ schützt dich. Du musst nicht hoffen, dass ein Wert vorhanden ist, weil der Zustand es garantiert. Das ist ein gutes Beispiel für Kotlin-Denken: Nicht null verstecken, sondern den Fall modellieren.
Für kleinere Situationen reicht oft eine frühe Rückgabe:
fun openUserDetails(userId: String?) {
val id = userId ?: return
// Ab hier ist id vom Typ String, nicht String?
navigateToUser(id)
}
Oder du gibst einen klaren Fehler zurück, wenn ein Wert fachlich notwendig ist:
fun requireUserId(arguments: Bundle): String {
return arguments.getString("userId")
?: error("Argument userId fehlt")
}
Auch diese Variante kann zur Laufzeit abbrechen, aber sie ist ehrlicher als arguments.getString("userId")!!. Die Fehlermeldung beschreibt den Vertrag. Du kannst sie in Tests prüfen. Du siehst im Stacktrace schneller, welcher fachliche Wert gefehlt hat.
Eine hilfreiche Entscheidungsregel lautet: Wenn du !! schreiben willst, halte kurz an und beantworte drei Fragen. Erstens: Warum ist der Typ nullable? Zweitens: Kann dieser Wert durch Lebenszyklus, externe Daten oder Timing wirklich fehlen? Drittens: Kann ich den Code so umbauen, dass der nicht-null Fall durch Typen, when, frühe Rückgabe oder Validierung entsteht? Wenn du eine dieser Fragen nicht sauber beantworten kannst, ist !! wahrscheinlich zu riskant.
Eine typische Stolperfalle ist der Gedanke: “Ich habe doch vorher geprüft.” Das kann stimmen, muss aber nicht stabil sein. Bei veränderbaren Properties kann sich ein Wert zwischen Prüfung und Zugriff ändern, besonders wenn mehrere Codepfade, Coroutines oder UI-Events beteiligt sind. Besser ist es, den Wert in eine lokale Variable zu übernehmen:
class SessionHolder {
var currentUser: User? = null
fun printName() {
val user = currentUser ?: return
println(user.displayName)
}
}
Die lokale Variable user ist nach der Prüfung nicht mehr nullable. Du nutzt die Smart-Cast-Fähigkeit von Kotlin und vermeidest einen erzwungenen Zugriff.
Eine weitere Stolperfalle entsteht bei Listen und Suchoperationen:
val selected = users.firstOrNull { it.id == selectedId }!!
Dieser Code crasht, wenn der Nutzer nicht gefunden wird. Das kann passieren, wenn Daten aktualisiert wurden, ein Filter aktiv ist oder der ausgewählte Eintrag gelöscht wurde. Besser ist eine bewusste Behandlung:
val selected = users.firstOrNull { it.id == selectedId }
if (selected == null) {
showMissingUserMessage()
return
}
openUser(selected)
In Tests solltest du nicht nur den schönen Pfad prüfen. Teste mindestens einen Fall, in dem der Wert fehlt. Bei einem ViewModel heißt das: Ladezustand, Erfolg und Fehlerzustand prüfen. Bei Argumenten heißt das: fehlendes Argument testen. Bei Mapping-Code heißt das: JSON-Feld fehlt oder ist null. So lernst du, ob dein Code wirklich mit nullable Daten umgehen kann.
Im Code-Review kannst du !! wie einen Marker behandeln. Jeder Fund braucht eine Begründung. Akzeptabel kann !! sein, wenn ein Framework-Vertrag sehr eng ist und du ihn direkt vorher validiert hast. Selbst dann ist requireNotNull(value) { "..." } oft besser, weil du eine verständliche Fehlermeldung gibst. In vielen Fällen ist eine Modelländerung sauberer: eigener UI-State, non-null Constructor-Parameter, validierter Input oder frühe Rückgabe.
Wichtig ist auch der Unterschied zwischen technischem Komfort und fachlicher Wahrheit. Wenn ein Profil ohne Nutzer nicht angezeigt werden kann, sollte der Screen keinen ProfileUiState(user: User?) als Dauerzustand herumtragen. Wenn ein Nutzer optional ist, darf dein UI nicht so tun, als sei er immer vorhanden. Null-Sicherheit hilft dir, diese Entscheidung im Code sichtbar zu machen.
Fazit
!! ist kein normales Kotlin-Werkzeug für den Alltag, sondern ein bewusstes Aussteigen aus der Null-Sicherheit. Du solltest es nur nutzen, wenn du den Vertrag wirklich kontrollierst und die Alternative klar schlechter wäre. Übe das an bestehendem Code: Suche nach !!, ersetze einige Stellen durch ?: return, when, let, requireNotNull mit Aussage oder ein besseres State-Modell, und prüfe die Änderung mit Tests oder im Debugger. Wenn du in Reviews erklären kannst, warum ein Wert nullable ist und wie dein Code den fehlenden Fall behandelt, bist du deutlich näher an stabilem Android-Code.