Bäume und Graphen in Android
Du lernst, wie Hierarchien und Beziehungen Android-Code strukturieren. Der Artikel zeigt dir Traversal an Navigation und Daten.
Bäume und Graphen sind keine reinen Theoriebegriffe aus dem Informatikstudium. Du nutzt sie in Android-Projekten ständig, auch wenn du sie nicht immer so benennst: eine Compose-Oberfläche hat verschachtelte Elemente, Navigation besteht aus verbundenen Screens, Module hängen voneinander ab, und Tests prüfen Pfade durch App-Logik. Wenn du diese Strukturen als Hierarchien, Beziehungen und Traversal denkst, findest du Fehler schneller und entwirfst klarere Architektur.
Was ist das?
Ein Baum ist eine Struktur aus Knoten mit einer klaren Richtung von oben nach unten. Es gibt eine Wurzel, darunter Kinder, darunter weitere Kinder. Ein Kind hat in einem klassischen Baum genau einen Elternknoten. Dieses Modell passt gut zu Hierarchien: ein Bildschirm enthält eine Spalte, die Spalte enthält Text, Buttons und Listen, und eine Liste enthält einzelne Zeilen. In Jetpack Compose denkst du oft genau so, auch wenn Compose intern deutlich mehr macht als nur eine simple Baumstruktur zu verwalten.
Ein Graph ist allgemeiner. Er besteht ebenfalls aus Knoten, aber die Verbindungen müssen keine strenge Eltern-Kind-Ordnung bilden. Ein Knoten kann mit vielen anderen Knoten verbunden sein. Es kann mehrere Wege zu einem Ziel geben, und es kann sogar Zyklen geben, also Wege, die wieder zu einem früheren Knoten zurückführen. Das passt zu Navigation, Abhängigkeiten zwischen Modulen, Datenbeziehungen oder Workflows: Ein Login-Screen kann zum Home-Screen führen, aber auch zur Registrierung, zur Passwort-Wiederherstellung oder zurück zur vorherigen Ansicht.
Der Unterschied ist wichtig, weil du mit einem Baum andere Annahmen treffen darfst als mit einem Graphen. In einem Baum kannst du meist sagen: „Wenn ich oben anfange und alle Kinder besuche, sehe ich jedes Element einmal.“ In einem Graphen stimmt das nicht automatisch. Dort brauchst du oft eine Menge bereits besuchter Knoten, damit du nicht in einer Schleife landest oder denselben Zustand mehrfach verarbeitest.
Für Android-Lernende ist das mentale Modell wichtiger als mathematische Vollständigkeit. Du musst nicht jede Graphentheorie-Definition auswendig können. Du solltest aber erkennen, ob du gerade eine klare Hierarchie oder ein Netz aus Beziehungen modellierst. Diese Unterscheidung wirkt sich darauf aus, wie du Datenklassen formulierst, wie du Navigation testest, wie du Compose-State nach unten gibst und wie du Abhängigkeiten zwischen Modulen kontrollierst.
Wie funktioniert es?
Ein Baum besteht aus Knoten und Kanten. Die Knoten sind die Elemente, die Kanten sind die Verbindungen. Bei einer UI-Hierarchie könnte der Root-Knoten dein App()-Composable sein. Darunter liegen Composables wie Scaffold, TopAppBar, NavHost oder ein Content-Bereich. Diese enthalten wieder weitere Composables. Wenn Compose neu zusammensetzt, betrachtet es diese Struktur nicht als statisches XML, sondern als Ergebnis deiner Composable-Funktionen und ihres aktuellen Zustands. Trotzdem hilft dir das Baumdenken: Daten fließen idealerweise von oben nach unten, Ereignisse gehen von unten nach oben.
Ein Graph funktioniert über Nachbarschaften. Du fragst nicht nur „Wer ist das Kind?“, sondern „Mit wem ist dieser Knoten verbunden?“ Bei Navigation kann ein Knoten ein Screen sein. Eine Kante ist dann eine erlaubte Navigation: von login zu home, von home zu settings, von settings zurück zu home. Bei Abhängigkeiten kann ein Knoten ein Modul sein: feature-profile hängt von core-ui und core-network ab. Wenn core-network plötzlich von feature-profile abhängt, entsteht ein Kreis, der deine Architektur schwer wartbar macht.
Traversal bedeutet, eine solche Struktur systematisch zu durchlaufen. Bei einem Baum ist das zum Beispiel Tiefensuche: Du gehst von einem Knoten zuerst in ein Kind, dann in dessen Kind, bis es nicht weitergeht. Danach gehst du zurück und nimmst das nächste Kind. Breitensuche macht es anders: Du besuchst zuerst alle direkten Kinder, dann die Enkel, dann die nächste Ebene. In Android brauchst du diese Begriffe selten als fertigen Algorithmus im Alltag, aber das Denken dahinter hilft beim Debuggen. Du fragst: Welcher Zustand wird zuerst gelesen? Welche UI-Knoten hängen davon ab? Welche Navigation kann nach welchem Ereignis passieren?
Bei Graphen ist Traversal vorsichtiger. Weil es Zyklen geben kann, brauchst du fast immer eine Prüfung auf bereits besuchte Knoten. Sonst kann ein rekursiver Durchlauf endlos werden. Das gilt nicht nur für eigenen Algorithmuscode. Es gilt auch als Denkregel für Architektur: Wenn Feature A Feature B kennt und Feature B wieder Feature A kennt, hast du eine Beziehung, die schwer zu testen und schwer zu ändern ist. Android-Qualität entsteht nicht nur durch fehlerfreien Code, sondern auch durch Strukturen, die du nachvollziehen, prüfen und in CI verlässlich testen kannst.
In Compose zeigt sich das auch beim State. Wenn du denselben Zustand an mehreren Stellen unabhängig hältst, baust du oft unbewusst einen Graphen aus Beziehungen, den du nicht mehr sauber überblickst. Ein Zustand ändert sich, aber ein anderer Zustand bleibt alt. Besser ist meistens: Der Zustand liegt an einer klaren Stelle, wird nach unten gereicht, und UI-Ereignisse werden nach oben gemeldet. Das ist kein Dogma für jeden Fall, aber eine sehr brauchbare Regel für Lernende und Junior-Devs.
In der Praxis
Stell dir eine kleine App mit drei Bereichen vor: Login, Übersicht und Details. Außerdem gibt es Einstellungen, die von der Übersicht und von den Details erreichbar sind. Als Baum wäre diese Struktur falsch beschrieben, weil settings nicht nur ein Kind eines einzigen Screens ist. Als Graph ist sie passend: Jeder Screen ist ein Knoten, jede erlaubte Navigation eine Kante.
Ein kleines Kotlin-Modell kann so aussehen:
data class RouteNode(
val route: String,
val nextRoutes: List<String>
)
val appGraph = listOf(
RouteNode("login", listOf("home")),
RouteNode("home", listOf("details", "settings")),
RouteNode("details", listOf("settings", "home")),
RouteNode("settings", listOf("home"))
)
fun reachableRoutesFrom(
start: String,
graph: List<RouteNode>
): Set<String> {
val byRoute = graph.associateBy { it.route }
val visited = mutableSetOf<String>()
val stack = ArrayDeque<String>()
stack.add(start)
while (stack.isNotEmpty()) {
val current = stack.removeLast()
if (!visited.add(current)) continue
val node = byRoute[current] ?: continue
node.nextRoutes.forEach { next ->
if (next !in visited) {
stack.add(next)
}
}
}
return visited
}
fun main() {
val routes = reachableRoutesFrom("login", appGraph)
check("settings" in routes)
check("details" in routes)
}
Das Beispiel ist bewusst klein. Es zeigt aber eine Denkweise, die du in echten Apps verwenden kannst: Du modellierst Beziehungen explizit und prüfst, welche Knoten erreichbar sind. In einer produktiven App würdest du Navigation mit Jetpack Navigation oder einer bestehenden Architektur lösen. Trotzdem bleibt die Frage gleich: Welche Screens gibt es? Welche Übergänge sind erlaubt? Welche Zustände müssen getestet werden?
Eine praktische Entscheidungsregel lautet: Wenn jedes Element genau einen logischen Besitzer hat, denke zuerst in Bäumen. Wenn ein Element aus mehreren Richtungen erreichbar ist oder mehrere gleichberechtigte Beziehungen hat, denke in Graphen. Eine Compose-UI ist meistens baumartig. Eine Navigationsstruktur ist meistens graphartig. Modulabhängigkeiten sind ebenfalls graphartig, sollten aber möglichst gerichtet und ohne unerwünschte Zyklen bleiben.
Eine typische Stolperfalle ist, einen Graphen wie einen Baum zu behandeln. Das passiert zum Beispiel, wenn du Navigation nur von der sichtbaren UI aus denkst: „Von diesem Button geht es dahin.“ Später kommen Deep Links, Benachrichtigungen, Zurück-Navigation oder mehrere Startpunkte dazu. Dann zeigt sich, dass ein Screen nicht sauber initialisiert wird, weil du nur einen Weg getestet hast. Besser ist, die möglichen Pfade bewusst zu notieren und kritische Pfade mit Tests abzusichern.
Bei strukturierten Daten gilt Ähnliches. Eine Kommentarliste mit Antworten auf Antworten ist ein Baum. Ein soziales Netzwerk aus Nutzern, Followern und Empfehlungen ist ein Graph. Wenn du beim Laden solcher Daten rekursiv arbeitest, brauchst du klare Grenzen: maximale Tiefe, bereits geladene IDs, Fehlerbehandlung und Tests für leere oder zyklische Daten. Auch wenn deine App keine komplexen Algorithmen enthält, sind diese Schutzmaßnahmen Teil solider Android-Entwicklung.
In Code-Reviews kannst du das Thema sehr konkret prüfen. Frage bei einer neuen Navigation: Ist jeder neue Screen erreichbar und auch wieder sinnvoll verlassbar? Gibt es Pfade, die denselben ViewModel-Zustand unterschiedlich initialisieren? Entstehen Modulabhängigkeiten von einer Feature-Schicht zurück in eine andere Feature-Schicht? Werden UI-Daten in einer klaren Hierarchie weitergegeben, oder dupliziert jede Ebene eigenen Zustand?
Für Tests kannst du klein anfangen. Schreibe Unit-Tests für Funktionen, die strukturierte Daten durchlaufen. Prüfe bei Navigation nicht nur den Standardpfad, sondern auch Fehlerpfade und Rückwege. In UI-Tests kannst du zentrale Traversal-Ideen anwenden: Starte bei einem Zustand, löse ein Ereignis aus, prüfe den nächsten sichtbaren Knoten der App. In CI sind solche Tests wertvoll, weil sie verhindern, dass ein späterer Umbau einen wichtigen Pfad unbemerkt unterbricht.
Auch beim Debuggen hilft dir das Modell. Wenn ein Composable falsche Daten zeigt, gehe den Baum nach oben: Wo wird der State erzeugt? Wo wird er verändert? Wo wird er als Parameter weitergegeben? Wenn eine Navigation falsch läuft, gehe den Graphen entlang: Von welchem Knoten kommst du? Welche Kante wurde ausgelöst? Ist das Ziel für diesen Zustand erlaubt? Diese Fragen sind oft produktiver als planloses Setzen vieler Logs.
Fazit
Bäume und Graphen geben dir eine präzise Sprache für Strukturen, die in Android täglich vorkommen: UI-Hierarchien, Navigation, Modulabhängigkeiten, Datenbeziehungen und Testpfade. Übe das nicht abstrakt, sondern an deinem eigenen Code: Zeichne für einen Screen den Compose-Baum, für deine App die wichtigsten Navigationsknoten und für deine Module die Abhängigkeiten. Prüfe dann mit Tests, Debugger oder Code-Review, ob deine Annahmen stimmen. Wenn du erkennst, wann du eine Hierarchie und wann du ein Netz aus Beziehungen vor dir hast, triffst du bessere Architekturentscheidungen und findest Fehler deutlich gezielter.