Debugging-Prozess in Android
Lerne, Android-Fehler mit Hypothesen, Belegen und Isolation gezielt zu untersuchen. So vermeidest du planlose Änderungen.
Ein guter Debugging-Prozess hilft dir, Fehler in Android-Apps gezielt zu finden, ohne den Code auf Verdacht umzubauen. Du arbeitest mit Hypothesen, sammelst Belege und isolierst den betroffenen Bereich, bis du die Ursache sauber erklären kannst. Das klingt langsamer als spontanes Ausprobieren, spart in realen Projekten aber Zeit, weil du nicht zufällig neue Fehler einbaust.
Was ist das?
Ein Debugging-Prozess ist eine feste Arbeitsweise zur Fehlersuche. Du beobachtest ein Problem, formulierst eine konkrete Vermutung, prüfst diese Vermutung mit passenden Belegen und änderst erst dann Code, wenn du die Ursache verstanden hast. Die drei wichtigen Begriffe sind Hypothese, Beleg und Isolation.
Eine Hypothese ist eine prüfbare Aussage. Nicht: „Compose ist kaputt.“ Besser: „Der Screen aktualisiert sich nicht, weil der StateFlow im ViewModel keinen neuen Wert ausgibt.“ Diese Aussage kannst du mit Logs, Breakpoints, Tests oder durch kontrollierte Eingaben prüfen.
Ein Beleg ist alles, was deine Vermutung stützt oder widerlegt: ein Stacktrace, ein Log-Eintrag, ein fehlgeschlagener Test, ein Screenshot, ein reproduzierbarer Ablauf, ein Wert im Debugger oder ein Eintrag aus Crash-Reporting. Wichtig ist, dass der Beleg beobachtbar ist. Ein Gefühl ist kein Beleg.
Isolation bedeutet, den Suchraum kleiner zu machen. In Android kann ein Fehler aus vielen Schichten kommen: Compose-UI, ViewModel, Repository, Room-Datenbank, Netzwerk, Coroutine, Navigation, Lifecycle oder Build-Konfiguration. Wenn du alles gleichzeitig betrachtest, wird die Fehlersuche unklar. Wenn du gezielt trennst, findest du schneller heraus, ob das Problem in der UI-Darstellung, in der Zustandslogik oder in einer Datenquelle liegt.
Im modernen Android-Kontext ist dieser Prozess besonders wichtig, weil Apps oft reaktiv aufgebaut sind. Daten fließen aus Repositories über Flows in ein ViewModel und von dort in Compose-State. Ein sichtbarer UI-Fehler ist dann nicht automatisch ein UI-Fehler. Vielleicht wird der falsche State erzeugt, vielleicht wird korrekt erzeugter State falsch gesammelt, vielleicht ist der Testdatensatz unvollständig. Der Debugging-Prozess schützt dich davor, an der erstbesten sichtbaren Stelle zu reparieren.
Wie funktioniert es?
Der Prozess beginnt mit einer klaren Reproduktion. Du musst wissen, wie du den Fehler auslöst. Beispiel: „App öffnen, Suche nach abc starten, Gerät drehen, Ergebnisliste ist leer.“ Wenn du den Fehler nicht reproduzieren kannst, dokumentierst du zumindest die bekannten Bedingungen: Android-Version, Gerät, Build-Variante, Netzwerkzustand, Nutzeraktion und relevante Daten.
Danach beschreibst du das erwartete und das tatsächliche Verhalten. Diese Trennung ist wichtig. „Die Liste ist kaputt“ ist ungenau. „Nach der Rotation sollte die letzte Suchanfrage erhalten bleiben; tatsächlich wird der Query-Text geleert und das Repository lädt keine Ergebnisse“ ist deutlich hilfreicher.
Dann formulierst du eine Hypothese. Eine gute Hypothese hat eine konkrete Ursache und eine erwartete Beobachtung. Zum Beispiel: „Wenn der Query-State nur mit remember gespeichert wird, geht er bei bestimmten Rekreationen verloren. Dann sollte rememberSaveable oder State im ViewModel das Verhalten stabilisieren.“ Du änderst aber noch nicht sofort den Code. Zuerst prüfst du, ob die Annahme stimmt.
Für die Prüfung sammelst du Belege. In Android nutzt du dafür mehrere Werkzeuge: Logcat für Laufzeitinformationen, den Debugger für konkrete Werte, Stacktraces für Abstürze, Unit-Tests für reine Logik, Instrumentation-Tests für Verhalten auf Gerät oder Emulator und Compose-Tests für UI-Zustände. Die Android-Testgrundlagen betonen, dass Tests nicht nur nachträglich Qualität sichern. Sie helfen dir auch, Fehler gezielt zu reproduzieren und später zu verhindern.
Isolation ist der nächste Schritt, wenn die Ursache noch unklar ist. Du kannst den Fehlerbereich verkleinern, indem du eine Schicht unabhängig prüfst. Wenn eine Compose-Liste leer bleibt, prüfst du zuerst, ob der UI-State überhaupt Elemente enthält. Falls ja, liegt das Problem eher in der Darstellung. Falls nein, prüfst du das ViewModel. Wenn das ViewModel korrekten State liefert, aber die UI nichts anzeigt, prüfst du die State-Erfassung in Compose. Wenn das ViewModel falschen State liefert, gehst du weiter zum Repository oder zur Datenquelle.
Ein Anfänger sollte sich dazu ein mentales Modell aufbauen: Debugging ist kein Raten, sondern ein kleiner wissenschaftlicher Zyklus. Beobachten, vermuten, prüfen, eingrenzen, ändern, erneut prüfen. Jede Runde sollte dein Wissen über das Problem verbessern. Wenn du nach einer Änderung nicht erklären kannst, warum sie helfen sollte, war die Änderung wahrscheinlich zu früh.
Im Alltag zeigt sich dieser Prozess oft bei typischen Android-Problemen: Ein Button reagiert nicht, ein Screen rendert zu oft, ein Flow gibt keinen Wert aus, ein Coroutine-Job wird abgebrochen, eine Navigation verliert Argumente, ein Test ist instabil oder ein Crash tritt nur auf bestimmten Geräten auf. In all diesen Fällen hilft dir dieselbe Struktur. Du musst nicht jedes API-Detail auswendig kennen, aber du brauchst eine saubere Spur von Beobachtungen.
Ein wichtiger Punkt ist Release-Qualität. Android-Qualität bedeutet nicht nur, dass die App auf deinem Gerät funktioniert. Sie soll auf vielen Geräten, Bildschirmgrößen, Android-Versionen und Nutzungswegen stabil bleiben. Ein strukturierter Debugging-Prozess führt dazu, dass du Ursachen dokumentierst, Tests ergänzt und Fehler nicht nur verdeckst. Das ist der Unterschied zwischen einer schnellen lokalen Korrektur und einer belastbaren Verbesserung.
In der Praxis
Stell dir vor, du hast eine Compose-App mit einer Aufgabenliste. Nach dem Tippen auf „Erledigt“ verschwindet eine Aufgabe nicht aus der Liste. Eine planlose Reaktion wäre, irgendwo mutableStateOf zu ändern, die Liste neu zu sortieren oder in der UI manuell zu filtern. Der bessere Weg beginnt mit einer Hypothese.
Hypothese: „Das Repository markiert die Aufgabe korrekt als erledigt, aber das ViewModel filtert erledigte Aufgaben nicht aus dem UI-State.“ Erwarteter Beleg: Im Repository ist isDone == true, im UI-State bleibt die Aufgabe trotzdem enthalten.
Ein vereinfachter Kotlin-Ausschnitt kann so aussehen:
data class Todo(
val id: Long,
val title: String,
val isDone: Boolean
)
data class TodoUiState(
val visibleTodos: List<Todo> = emptyList()
)
class TodoViewModel(
private val repository: TodoRepository
) : ViewModel() {
val uiState: StateFlow<TodoUiState> =
repository.todos
.map { todos ->
TodoUiState(
visibleTodos = todos.filter { todo -> !todo.isDone }
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TodoUiState()
)
fun markDone(id: Long) {
viewModelScope.launch {
repository.markDone(id)
}
}
}
Wenn der Fehler in dieser Logik vermutet wird, kannst du ihn isoliert testen. Du brauchst dafür nicht zuerst einen kompletten UI-Test. Ein Unit-Test für das ViewModel oder für die Mapping-Funktion kann reichen, weil du die Frage eingrenzt: Wird aus den Daten der richtige UI-State?
@Test
fun doneTodosAreNotVisible() = runTest {
val repository = FakeTodoRepository(
initialTodos = listOf(
Todo(id = 1, title = "Debugging ueben", isDone = false),
Todo(id = 2, title = "Test schreiben", isDone = true)
)
)
val viewModel = TodoViewModel(repository)
val state = viewModel.uiState.first { it.visibleTodos.isNotEmpty() }
assertEquals(listOf(1L), state.visibleTodos.map { it.id })
}
Dieser Test ist ein Beleg. Wenn er fehlschlägt, liegt die Ursache wahrscheinlich in der State-Erzeugung oder in der Testumgebung. Wenn er besteht, verschiebst du die Hypothese: Vielleicht erhält die UI den State nicht, vielleicht wird ein falscher ViewModel-Scope verwendet, vielleicht wird in Compose eine lokale Liste angezeigt, die nicht aus uiState stammt.
Dann isolierst du die UI. In Compose prüfst du, ob der Screen wirklich den State verwendet, der aus dem ViewModel kommt. Ein häufiger Fehler ist doppelter State: Du hast eine Liste im ViewModel, speicherst aber zusätzlich eine lokale Kopie mit remember. Diese Kopie wird nicht automatisch aktualisiert, wenn sich der eigentliche State ändert.
Problematisches Muster:
@Composable
fun TodoScreen(uiState: TodoUiState) {
val localTodos = remember { mutableStateListOf<Todo>() }
LazyColumn {
items(localTodos) { todo ->
Text(todo.title)
}
}
}
Hier ist die UI vom übergebenen uiState entkoppelt. Der Screen kann korrekt neu zusammengesetzt werden und trotzdem falsche Daten zeigen. Eine bessere Variante nutzt den State direkt:
@Composable
fun TodoScreen(uiState: TodoUiState) {
LazyColumn {
items(
items = uiState.visibleTodos,
key = { todo -> todo.id }
) { todo ->
Text(todo.title)
}
}
}
Die praktische Entscheidungsregel lautet: Ändere nur eine Sache pro Debugging-Runde, und schreibe vorher auf, welche Beobachtung du erwartest. Wenn du gleichzeitig Repository, UI und Coroutine-Startverhalten änderst, weißt du nachher nicht, welche Änderung relevant war. Dadurch wird der Fehler vielleicht verdeckt, aber dein Verständnis bleibt schwach.
Eine typische Stolperfalle ist das „Fixen“ durch zusätzliche Aktualisierungen. Du siehst, dass die UI nicht neu lädt, und rufst irgendwo erneut loadTodos() auf. Vielleicht wirkt das im Emulator, aber die eigentliche Ursache bleibt bestehen: falscher State-Besitz, fehlende Flow-Emission, ein Lifecycle-Problem oder eine unsaubere Datenquelle. Solche Korrekturen erzeugen später doppelte Requests, flackernde UI oder instabile Tests.
Eine zweite Stolperfalle ist unpräzises Logging. Log.d("Test", "geht nicht") hilft kaum. Besser sind Logs, die deine Hypothese prüfen:
Log.d("TodoDebug", "markDone id=$id")
Log.d("TodoDebug", "visible count=${state.visibleTodos.size}")
Log.d("TodoDebug", "repository emitted count=${todos.size}")
Diese Logs sollten in produktivem Code nicht dauerhaft verstreut bleiben. Sie sind Werkzeuge zur Untersuchung. Wenn du aus ihnen eine dauerhafte Erkenntnis gewinnst, sollte sie eher in einen Test, eine bessere Fehlerbehandlung oder eine klare Struktur im Code einfließen.
Auch Code-Reviews profitieren von diesem Denken. Statt nur zu schreiben „funktioniert bei mir nicht“, kannst du präzise kommentieren: „Die Änderung behebt den UI-Fall, aber ich sehe keinen Beleg, dass das Repository nach markDone einen neuen Flow-Wert ausgibt. Bitte ergänze einen Test oder zeige, wo die Emission passiert.“ So wird Review-Arbeit sachlicher und nützlicher.
Fazit
Ein guter Debugging-Prozess macht dich als Android-Entwickler verlässlicher, weil du nicht nur Symptome veränderst, sondern Ursachen findest. Formuliere bei deinem nächsten Fehler zuerst eine Hypothese, sammle einen klaren Beleg und isoliere dann die betroffene Schicht. Prüfe die Korrektur anschließend mit einem Test, im Debugger oder in einem kurzen Code-Review gegen deine ursprüngliche Annahme. So trainierst du nicht nur ein einzelnes Problem weg, sondern baust eine Arbeitsweise auf, die bei Compose, Jetpack, Coroutines, Architekturfragen und Release-Qualität tragfähig bleibt.