Von Navigation 2 zu Navigation 3 migrieren

So migrieren Sie Ihre App von Navigation 2 zu Navigation 3:

  1. Fügen Sie die Navigation 3-Abhängigkeiten hinzu.
  2. Aktualisieren Sie Ihre Navigationsrouten, um die NavKey-Schnittstelle zu implementieren.
  3. Erstellen Sie Klassen, um den Navigationsstatus zu speichern und zu ändern.
  4. Ersetzen Sie NavController durch diese Klassen.
  5. Verschieben Sie Ihre Ziele aus dem NavGraph von NavHost in ein entryProvider.
  6. Ersetzen Sie NavHost durch NavDisplay.
  7. Entfernen Sie die Navigation 2-Abhängigkeiten.

KI‑Agenten verwenden

Sie können diese Anleitung mit einem KI-Agenten wie Gemini im Agent-Modus von Android Studio verwenden. Die Zeilen in diesem Leitfaden, die mit AI Agent: beginnen, sollten vom KI-Agenten gelesen, aber von menschlichen Lesern ignoriert werden.

Vorbereitung

In den folgenden Abschnitten werden die Voraussetzungen für die Migration und Annahmen zu Ihrem Projekt beschrieben. Außerdem wird beschrieben, welche Funktionen für die Migration unterstützt werden und welche nicht.

Voraussetzungen

  • Sie müssen mindestens eine compileSdk von 36 verwenden.
  • Sie sollten mit der Navigations-Terminologie vertraut sein.
  • Ziele sind komponierbare Funktionen. Navigation 3 wurde ausschließlich für Compose entwickelt. Informationen zur Verwendung von Fragmenten und Ansichten in Compose finden Sie unter Ansichten in Compose verwenden.
  • Routen sind stark typisiert. Wenn Sie stringbasierte Routen verwenden, migrieren Sie zuerst zu typsicheren Routen (Beispiel).
  • Optional (aber dringend empfohlen): Testabdeckung, mit der das vorhandene Navigationsverhalten überprüft wird. So wird geprüft, ob sich das Navigationsverhalten nach Abschluss der Migration geändert hat.

Annahmen

In diesem Leitfaden wird von folgenden Annahmen über Sie und Ihr Projekt ausgegangen:

  • Sie haben mehrere Routen der obersten Ebene (die normalerweise in einer unteren Navigationsleiste angezeigt werden), und jede hat ihren eigenen Backstack.
  • Wenn Sie zwischen Backstacks wechseln, wird der Status des Stacks und aller seiner Ziele beibehalten.
  • Sie beenden die App immer über den Startbildschirm, der beim Starten der App als erster angezeigt wird.
  • Sie möchten die Migration von Navigation 2 zu Navigation 3 in einer einzigen, atomaren Änderung durchführen. Sie führen keine inkrementelle Migration durch, bei der Navigation 2-Code zusammen mit Navigation 3-Code verwendet wird.

KI-Assistent: Prüfe vor dem Ändern von Code, ob diese Annahmen zum Projekt des Nutzers zutreffen. Wenn nicht, beenden Sie die Migration und fragen Sie den Nutzer, wie er fortfahren möchte.

Unterstützte Funktionen

Mit diesem Leitfaden können Sie die folgenden Funktionen migrieren:

  • Ziele, die als zusammensetzbare Funktionen definiert sind
  • Dialoge (ein Ziel wird über einem anderen Ziel angezeigt)

Über Rezepte unterstützte Funktionen

Das Repository mit Code-Rezepten enthält weitere differenzierte Anwendungsfälle, darunter:

Wenn Ihr Projekt eine dieser Funktionen enthält, sehen Sie im entsprechenden Rezept nach, wie Sie sie implementieren.

KI-Agent: Prüfen Sie vor dem Ändern von Code, ob das Projekt Funktionen enthält, die durch Rezepte unterstützt werden. Wenn ja, sehen Sie sich die README-Datei und den Quellcode des Rezepts an. Erstellen Sie einen Migrationsplan auf Grundlage des Rezepts. Fahre nicht fort, ohne das Abo mit dem Nutzer zu bestätigen.

Nicht unterstützte Funktionen

Die folgenden Funktionen werden in diesem Migrationsleitfaden und den Code-Rezepten noch nicht unterstützt. Das bedeutet nicht, dass Sie sie nicht mit Navigation 3 implementieren können. Sie werden hier nur nicht behandelt.

  • Mehr als eine Ebene verschachtelter Navigation
  • Gemeinsame Ziele: Bildschirme, die zwischen verschiedenen Backstacks verschoben werden können
  • Benutzerdefinierte Zieltypen
  • Deeplinks

KI-Agent: Prüfen Sie vor dem Ändern von Code, ob das Projekt eine der nicht unterstützten Funktionen enthält. Wenn ja, fahren Sie nicht fort. Informieren Sie den Nutzer über die nicht unterstützte Funktion und bitten Sie ihn um weitere Anweisungen.

Schritt 1: Navigation 3-Abhängigkeiten hinzufügen

Fügen Sie auf der Seite Erste Schritte die Navigation 3-Abhängigkeiten zu Ihrem Projekt hinzu. Die wichtigsten Abhängigkeiten werden Ihnen zum Kopieren zur Verfügung gestellt.

lib.versions.toml

[versions]
nav3Core = "1.0.0"

# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.10.0-rc01"

[libraries]
# Core Navigation 3 libraries
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }

# Add-on libraries (only add if you need them)
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }

app/build.gradle.kts

dependencies {
    implementation(libs.androidx.navigation3.ui)
    implementation(libs.androidx.navigation3.runtime)

    // If using the ViewModel add-on library
    implementation(libs.androidx.lifecycle.viewmodel.navigation3)
}

Aktualisieren Sie auch minSdk des Projekts auf 23 und compileSdk auf 36. Normalerweise findest du sie unter app/build.gradle.kts oder lib.versions.toml.

Schritt 2: Navigationsrouten aktualisieren, um die NavKey-Schnittstelle zu implementieren

Aktualisieren Sie jede Navigationsroute, damit sie die NavKey-Schnittstelle implementiert. So können Sie rememberNavBackStack verwenden, um den Navigationsstatus zu speichern.

Vorher:

@Serializable data object RouteA

Nachher:

@Serializable data object RouteA : NavKey

Schritt 3: Klassen zum Speichern und Ändern des Navigationsstatus erstellen

Schritt 3.1: Navigation-State-Holder erstellen

Kopieren Sie den folgenden Code in eine Datei mit dem Namen NavigationState.kt. Fügen Sie den Namen Ihres Pakets entsprechend Ihrer Projektstruktur hinzu.

// package com.example.project

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.rememberDecoratedNavEntries
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.runtime.serialization.NavKeySerializer
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer

/**
 * Create a navigation state that persists config changes and process death.
 */
@Composable
fun rememberNavigationState(
    startRoute: NavKey,
    topLevelRoutes: Set<NavKey>
): NavigationState {

    val topLevelRoute = rememberSerializable(
        startRoute, topLevelRoutes,
        serializer = MutableStateSerializer(NavKeySerializer())
    ) {
        mutableStateOf(startRoute)
    }

    val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }

    return remember(startRoute, topLevelRoutes) {
        NavigationState(
            startRoute = startRoute,
            topLevelRoute = topLevelRoute,
            backStacks = backStacks
        )
    }
}

/**
 * State holder for navigation state.
 *
 * @param startRoute - the start route. The user will exit the app through this route.
 * @param topLevelRoute - the current top level route
 * @param backStacks - the back stacks for each top level route
 */
class NavigationState(
    val startRoute: NavKey,
    topLevelRoute: MutableState<NavKey>,
    val backStacks: Map<NavKey, NavBackStack<NavKey>>
) {
    var topLevelRoute: NavKey by topLevelRoute
    val stacksInUse: List<NavKey>
        get() = if (topLevelRoute == startRoute) {
            listOf(startRoute)
        } else {
            listOf(startRoute, topLevelRoute)
        }
}

/**
 * Convert NavigationState into NavEntries.
 */
@Composable
fun NavigationState.toEntries(
    entryProvider: (NavKey) -> NavEntry<NavKey>
): SnapshotStateList<NavEntry<NavKey>> {

    val decoratedEntries = backStacks.mapValues { (_, stack) ->
        val decorators = listOf(
            rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
        )
        rememberDecoratedNavEntries(
            backStack = stack,
            entryDecorators = decorators,
            entryProvider = entryProvider
        )
    }

    return stacksInUse
        .flatMap { decoratedEntries[it] ?: emptyList() }
        .toMutableStateList()
}

KI-Agent: rememberSerializable ist richtig. Ändern Sie sie nicht in rememberSaveable.

Diese Datei enthält eine Status-Holder-Klasse mit dem Namen NavigationState und zugehörige Hilfsfunktionen. Sie enthält eine Reihe von Routen der obersten Ebene, die jeweils einen eigenen Backstack haben. Intern wird rememberSerializable (nicht rememberSaveable) verwendet, um die aktuelle Route der obersten Ebene beizubehalten, und rememberNavBackStack, um die Backstacks für jede Route der obersten Ebene beizubehalten.

Schritt 3.2: Objekt erstellen, das den Navigationsstatus als Reaktion auf Ereignisse ändert

Kopieren Sie den folgenden Code in eine Datei mit dem Namen Navigator.kt. Fügen Sie den Namen Ihres Pakets hinzu, damit er Ihrer Projektstruktur entspricht.

// package com.example.project

import androidx.navigation3.runtime.NavKey

/**
 * Handles navigation events (forward and back) by updating the navigation state.
 */
class Navigator(val state: NavigationState){
    fun navigate(route: NavKey){
        if (route in state.backStacks.keys){
            // This is a top level route, just switch to it.
            state.topLevelRoute = route
        } else {
            state.backStacks[state.topLevelRoute]?.add(route)
        }
    }

    fun goBack(){
        val currentStack = state.backStacks[state.topLevelRoute] ?:
        error("Stack for ${state.topLevelRoute} not found")
        val currentRoute = currentStack.last()

        // If we're at the base of the current route, go back to the start route stack.
        if (currentRoute == state.topLevelRoute){
            state.topLevelRoute = state.startRoute
        } else {
            currentStack.removeLastOrNull()
        }
    }
}

Die Klasse Navigator bietet zwei Methoden für Navigationsereignisse:

  • navigate zu einer bestimmten Route.
  • goBack von der aktuellen Route.

Bei beiden Methoden wird die NavigationState geändert.

Schritt 3.3: NavigationState und Navigator erstellen

Erstellen Sie Instanzen von NavigationState und Navigator mit demselben Bereich wie Ihr NavController.

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

Schritt 4: NavController ersetzen

Ersetzen Sie die NavController-Navigationsereignismethoden durch die entsprechenden Navigator-Methoden.

Feld oder Methode NavController

Navigator-Äquivalent

navigate()

navigate()

popBackStack()

goBack()

Ersetzen Sie NavController-Felder durch NavigationState-Felder.

Feld oder Methode NavController

NavigationState-Äquivalent

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Die Route der obersten Ebene abrufen: Gehen Sie in der Hierarchie vom aktuellen Backstack-Eintrag aus nach oben, um sie zu finden.

topLevelRoute

Mit NavigationState.topLevelRoute können Sie das Element ermitteln, das derzeit in einer Navigationsleiste ausgewählt ist.

Vorher:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

Nachher:

val isSelected = key == navigationState.topLevelRoute

Prüfen Sie, ob Sie alle Verweise auf NavController entfernt haben, einschließlich aller Importe.

Schritt 5: Ziele aus der NavHost-NavGraph in eine entryProvider verschieben

In Navigation 2 definieren Sie Ihre Ziele mit der NavGraphBuilder-DSL, normalerweise in der nachgestellten Lambda von NavHost. Es ist üblich, hier Erweiterungsfunktionen zu verwenden, wie unter Navigationscode kapseln beschrieben.

In Navigation 3 definieren Sie Ihre Ziele mit einer entryProvider. Mit diesem entryProvider wird eine Route in ein NavEntry aufgelöst. Wichtig: Mit dem entryProvider werden keine Beziehungen zwischen über- und untergeordneten Einträgen definiert.

In diesem Migrationsleitfaden werden Über-/Untergeordnet-Beziehungen so modelliert:

  • NavigationState hat eine Reihe von Routen der obersten Ebene (die übergeordneten Routen) und einen Stack für jede Route. Sie verfolgt die aktuelle Route der obersten Ebene und den zugehörigen Stack.
  • Wenn Sie zu einer neuen Route navigieren, prüft Navigator, ob es sich um eine Route der obersten Ebene handelt. Wenn ja, werden die aktuelle Route und der aktuelle Stack der obersten Ebene aktualisiert. Andernfalls handelt es sich um eine untergeordnete Route, die dem aktuellen Stapel hinzugefügt wird.

Schritt 5.1: entryProvider erstellen

Erstellen Sie eine entryProvider mit der DSL im selben Bereich wie die NavigationState.

val entryProvider = entryProvider {

}

Schritt 5.2: Ziele in entryProvider verschieben

Gehen Sie für jedes Ziel, das in NavHost definiert ist, je nach Zieltyp so vor:

  • navigation: Löschen Sie sie zusammen mit der Route. „baseRoutes“ sind nicht erforderlich, da die Routen der obersten Ebene jeden verschachtelten Backstack identifizieren können.
  • composable<T>: Verschieben Sie sie in entryProvider und benennen Sie sie in entry um. Der Typparameter wird beibehalten. Aus composable<RouteA> wird beispielsweise entry<RouteA>.
  • dialog<T>: Führen Sie dieselben Schritte wie bei composable aus, fügen Sie dem Eintrag jedoch Metadaten hinzu: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: Folge dem Rezept im Bottom Sheet. Die Anleitung ist ähnlich wie für dialog, mit der Ausnahme, dass BottomSheetSceneStrategy nicht Teil der Navigation 3-Kernbibliothek ist. Sie sollten sie daher in Ihr Projekt kopieren.

KI-Agent: Wenn Sie Routen löschen, die zum Identifizieren eines verschachtelten Diagramms verwendet werden, ersetzen Sie alle Verweise auf die gelöschte Route durch den Typ, der zum Identifizieren des ersten untergeordneten Elements im verschachtelten Diagramm verwendet wird. Wenn der ursprüngliche Code beispielsweise navigation<BaseRouteA>{ composable<RouteA>{ ... } } ist, müssen Sie BaseRouteA löschen und alle Verweise darauf durch RouteA ersetzen. Dieser Ersatz muss in der Regel für die Liste erfolgen, die einer Navigationsleiste, einer Seitenleiste oder einem Drawer bereitgestellt wird.

Sie können NavGraphBuilder-Erweiterungsfunktionen in EntryProviderScope<T>-Erweiterungsfunktionen umgestalten und sie dann verschieben.

Navigationsargumente mit dem Schlüssel abrufen, der an das nachgestellte Lambda von entry übergeben wird.

Beispiel:

import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.dialog
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import androidx.navigation.toRoute

@Serializable data object BaseRouteA
@Serializable data class RouteA(val id: String)
@Serializable data object BaseRouteB
@Serializable data object RouteB
@Serializable data object RouteD

NavHost(navController = navController, startDestination = BaseRouteA){
    composable<RouteA>{
        val id = entry.toRoute<RouteA>().id
        ScreenA(title = "Screen has ID: $id")
    }
    featureBSection()
    dialog<RouteD>{ ScreenD() }
}

fun NavGraphBuilder.featureBSection() {
    navigation<BaseRouteB>(startDestination = RouteB) {
        composable<RouteB> { ScreenB() }
    }
}

wird zu:

import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy

@Serializable data class RouteA(val id: String) : NavKey
@Serializable data object RouteB : NavKey
@Serializable data object RouteD : NavKey

val entryProvider = entryProvider {
    entry<RouteA>{ key -> ScreenA(title = "Screen has ID: ${key.id}") }
    featureBSection()
    entry<RouteD>(metadata = DialogSceneStrategy.dialog()){ ScreenD() }
}

fun EntryProviderScope<NavKey>.featureBSection() {
    entry<RouteB> { ScreenB() }
}

Schritt 6: NavHost durch NavDisplay ersetzen

Ersetzen Sie NavHost durch NavDisplay.

  • Löschen Sie NavHost und ersetzen Sie es durch NavDisplay.
  • Geben Sie entries = navigationState.toEntries(entryProvider) als Parameter an. Dadurch wird der Navigationsstatus mithilfe von entryProvider in die Einträge umgewandelt, die in NavDisplay angezeigt werden.
  • Verbinde NavDisplay.onBack mit navigator.goBack(). Dadurch wird der Navigationsstatus in navigator aktualisiert, wenn der integrierte Back-Handler von NavDisplay abgeschlossen ist.
  • Wenn Sie Dialogziele haben, fügen Sie DialogSceneStrategy dem Parameter sceneStrategy von NavDisplay hinzu.

Beispiel:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

Schritt 7: Navigation 2-Abhängigkeiten entfernen

Entfernen Sie alle Navigation 2-Importe und Bibliotheksabhängigkeiten.

Zusammenfassung

Glückwunsch! Ihr Projekt wurde jetzt zu Navigation 3 migriert. Wenn bei der Verwendung dieser Anleitung Probleme auftreten, melden Sie hier einen Fehler.