Android Coden
Android 4 min lesen

Navigation Architecture

Navigation Architecture definiert, wie Screens als Ziele und Routen strukturiert werden. Feature-Grenzen bleiben so testbar und klar abgegrenzt.

Navigation ist mehr als das Wechseln zwischen Screens. In modernen Android-Apps entscheidet die Navigationsarchitektur darüber, ob Features isoliert entwickelt und getestet werden können – oder ob sich Routen, Back-Stack-Regeln und Datenübergaben zu einem schwer wartbaren Geflecht verweben. Der Jetpack Navigation Component gibt dir ein Werkzeug, das Ziele, Routen und Graphen explizit und nachvollziehbar macht.

Was ist das?

Navigation Architecture beschreibt das Prinzip, alle Navigations-Ziele (Destinations), Übergänge (Routes) und Hierarchiegrenzen (Feature Boundaries) einer App als eine zusammenhängende, versionierbare Struktur zu modellieren. Das Gegenteil ist implizite Navigation: Ein Screen startet direkt einen anderen Screen, kennt dessen Kontext und schleust Daten per Intent-Extra weiter. Diese Kopplung macht Änderungen teuer und Unit-Tests nahezu unmöglich.

Im Jetpack-Ökosystem materialisiert sich Navigation Architecture als NavGraph. Ein Graph enthält eine Menge von NavDestination-Knoten – in Compose sind das typisierte Route-Objekte – und die erlaubten Übergänge zwischen ihnen. Der NavController ist die einzige Schnittstelle, über die du zur Laufzeit navigierst. Damit liegt die gesamte Navigationslogik an einem Ort und nicht verteilt auf zwanzig startActivity()-Aufrufe quer durch die Codebasis.

Das Konzept der Feature Boundary ergänzt den Graphen: Statt alle Destinations in einen flachen Graphen zu werfen, bekommt jedes Feature einen eigenen Sub-Graphen. Andere Features kennen nur den Einstiegspunkt, nicht die innere Struktur. Das ist die direkte Übersetzung des Modularisierungsprinzips auf die Navigationsebene.

Wie funktioniert es?

Typisierte Routen

Seit Navigation 2.8 (Kotlin Serialization) kannst du Routen als @Serializable-Kotlin-Objekte definieren. Eine Destination ist dann kein fehleranfälliger String mehr, sondern ein Typ, den der Compiler prüft:

@Serializable
object HomeRoute

@Serializable
data class DetailRoute(val articleId: String)

Du registrierst diese Routen im NavHost:

NavHost(navController = navController, startDestination = HomeRoute) {
    composable<HomeRoute> {
        HomeScreen(onArticleClick = { id ->
            navController.navigate(DetailRoute(id))
        })
    }
    composable<DetailRoute> { backStackEntry ->
        val route = backStackEntry.toRoute<DetailRoute>()
        DetailScreen(articleId = route.articleId)
    }
}

Falsch geschriebene Route-Namen oder fehlende Argumente werden jetzt vom Compiler erkannt, nicht erst zur Laufzeit.

Verschachtelte Graphen für Feature Boundaries

Sobald eine App mehrere Feature-Module hat, schachtelst du deren Graphen mit dem navigation-Builder:

NavHost(navController, startDestination = OnboardingGraph) {
    navigation<OnboardingGraph>(startDestination = WelcomeRoute) {
        composable<WelcomeRoute> { ... }
        composable<PermissionsRoute> { ... }
    }
    navigation<MainGraph>(startDestination = HomeRoute) {
        composable<HomeRoute> { ... }
        composable<DetailRoute> { ... }
    }
}

Das Feature Main kennt den internen Aufbau von Onboarding nicht. Wenn sich der Onboarding-Fluss ändert – ein Screen kommt hinzu, einer fällt weg – muss Main nicht angefasst werden.

Back Stack und popUpTo

Der Back Stack ist ein Schlüsselkonzept. Ohne explizite popUpTo-Konfiguration häufst du Destinations auf dem Stack auf. Beim Abschluss des Onboardings zum Beispiel willst du den gesamten Onboarding-Graphen leeren, damit der Nutzer nicht zurücknavigieren kann:

navController.navigate(MainGraph) {
    popUpTo<OnboardingGraph> { inclusive = true }
    launchSingleTop = true
}

inclusive = true entfernt auch den Startknoten des Graphen. launchSingleTop verhindert, dass die Ziel-Destination doppelt auf den Stack gelegt wird, wenn der Nutzer schnell mehrfach navigiert.

In der Praxis

Konkretes Beispiel: Onboarding sauber trennen

Stell dir vor, deine App hat drei Onboarding-Screens und danach die Haupt-App. Ein häufiger Fehler ist, alle Destinations in einen einzigen flachen Graphen zu legen. Der Nutzer schließt das Onboarding ab – und kann mit der Zurück-Taste trotzdem wieder zu Screen 2 des Onboardings gelangen, weil die Entries noch auf dem Stack liegen.

Die Lösung ist der verschachtelte OnboardingGraph mit popUpTo<OnboardingGraph> { inclusive = true } beim Abschluss. Der gesamte Onboarding-Stack wird geleert; der Nutzer landet auf dem Haupt-Screen ohne Rückweg. Überprüfen kannst du das in einem Instrumentierten Test mit TestNavHostController:

@Test
fun onboardingFinish_clearsBackStack() {
    val navController = TestNavHostController(context)
    // Graph aufbauen, zu FinishOnboardingRoute navigieren
    // ...
    assertThat(navController.currentDestination?.route)
        .isEqualTo(HomeRoute::class.qualifiedName)
    assertThat(navController.backQueue.size).isEqualTo(2) // Root + Home
}

Typische Stolperfalle: NavController im ViewModel

Ein sehr häufiger Fehler ist, den NavController direkt in ein ViewModel zu injizieren oder als Parameter weiterzugeben. Das koppelt dein ViewModel an die UI-Schicht und macht Unit-Tests schmerzhaft, weil du einen echten NavController brauchst – der wiederum eine Activity voraussetzt.

Die korrekte Strategie: Das ViewModel emittiert ein einmaliges Ereignis über ein Channel oder SharedFlow, und die Composable-Funktion abonniert dieses und ruft navController.navigate(...) selbst auf. Das ViewModel bleibt damit ein reines Kotlin-Objekt, das du ohne Android-Framework testen kannst.

Fazit

Navigation Architecture ist keine Bibliotheksfrage, sondern eine bewusste Designentscheidung: Wo liegen die Grenzen deiner Graphen, wer darf navigieren und wie wird der Back Stack kontrolliert? Mit typisierten Routen, verschachtelten Graphen und einer klaren Trennung zwischen Navigationsentscheidung (ViewModel) und Navigationsauführung (UI) kannst du Feature-Module isoliert entwickeln und testen. Schau dir deine aktuelle App an: Gibt es Destinations, die direkt auf die innere Struktur eines anderen Features zeigen? Schreib einen Test mit TestNavHostController, navigiere zu jeder wichtigen Destination und prüfe, ob der Back Stack danach den erwarteten Zustand hat. Dieses kleine Experiment zeigt dir sofort, wo deine Navigationsarchitektur noch Klarheit braucht.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.