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 von NavHosts NavGraph in einen entryProvider.
  6. Ersetzen Sie NavHost durch NavDisplay.
  7. Entfernen Sie die Navigation 2-Abhängigkeiten.

Von Navigation 2 zu Navigation 3 migrieren

Mit diesem Prompt wird diese Anleitung verwendet, um zu Navigation 3 zu migrieren.

Migrate from Navigation 2 to Navigation 3 using the official
migration guide.

KI-Prompts verwenden

KI-Prompts sind für die Verwendung in Gemini in Android Studio vorgesehen.

Weitere Informationen zu Gemini in Studio finden Sie hier: https://developer.android.com/studio/gemini/overview

Wenn Probleme auftreten, melden Sie sie hier.

Vorbereitung

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

Vorbereitung

  • Sie müssen compileSdk 36 oder höher verwenden.
  • Sie sollten mit der Navigationsterminologie vertraut sein.
  • Ziele sind zusammensetzbare Funktionen. Navigation 3 ist ausschließlich für Compose konzipiert. 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 zu typsicheren Routen zuerst (Beispiel).
  • Optional, aber sehr empfehlenswert: Testabdeckung, mit der das vorhandene Navigationsverhalten überprüft wird. So lässt sich prüfen, ob sich das Navigationsverhalten nach Abschluss der Migration geändert hat.

Annahmen

In dieser Anleitung werden die folgenden Annahmen zu Ihnen und Ihrem Projekt getroffen:

  • Sie haben eine oder mehrere Routen auf oberster Ebene (die normalerweise in einer unteren Navigationsleiste angezeigt werden) und jede hat ihren eigenen Back-Stack.
  • Wenn Sie zwischen Back-Stacks wechseln, bleiben der Status des Stacks und alle zugehörigen Ziele erhalten.
  • Sie beenden die App immer über den Startbildschirm. Das ist der erste Bildschirm, der beim Starten der App 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-Agent: Prüfen Sie 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 Sie vorgehen sollen.

Unterstützte Funktionen

Mit dieser Anleitung können Sie die folgenden Funktionen migrieren:

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

Funktionen, die über Rezepte unterstützt werden

Das Code-Rezepte-Repository enthält detailliertere Anwendungs fälle, darunter:

Wenn Ihr Projekt eine dieser Funktionen enthält, lesen Sie das entsprechende Rezept, um zu erfahren, wie Sie sie implementieren.

KI-Agent: Prüfen Sie vor dem Ändern von Code, ob das Projekt Funktionen enthält, die über Rezepte unterstützt werden. Wenn ja, lesen Sie die README-Datei und den Quellcode des Rezepts. Erstellen Sie einen Migrationsplan basierend auf dem Rezept. Fahren Sie erst fort, wenn der Nutzer den Plan bestätigt hat.

Nicht unterstützte Funktionen

Diese Migrationsanleitung und die Code-Rezepte unterstützen die folgenden Funktionen noch nicht. 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 Back-Stacks verschoben werden können
  • Benutzerdefinierte Zieltypen
  • Deep links

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 um weitere Anweisungen.

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

Auf der Seite Erste Schritte können Sie die Navigation 3-Abhängigkeiten zu Ihrem Projekt hinzufügen. Die Kernabhängigkeiten können Sie kopieren.

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 außerdem minSdk des Projekts auf 23 und compileSdk auf 36. Diese finden Sie normalerweise in app/build.gradle.kts oder lib.versions.toml.

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

Aktualisieren Sie jede Navigations-route so, dass 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 erstellen, um den Navigationsstatus zu speichern und zu ändern

Schritt 3.1: Navigationsstatus-Holder erstellen

Kopieren Sie den folgenden Code in eine Datei mit dem Namen NavigationState.kt. Fügen Sie Ihren Paketnamen hinzu, damit er Ihrer Projektstruktur entspricht.

// 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 korrekt. Ändern Sie es 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 auf oberster Ebene, jede mit ihrem eigenen Back-Stack. Intern wird rememberSerializable (nicht rememberSaveable) verwendet, um die aktuelle Route auf oberster Ebene beizubehalten, und rememberNavBackStack, um die Back-Stacks für jede Route auf oberster 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 Ihren Paketnamen 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.

Beide Methoden ändern NavigationState.

Schritt 3.3: NavigationState und Navigator erstellen

Erstellen Sie Instanzen von NavigationState und Navigator mit demselben Bereich wie 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 Methoden für Navigationsereignisse von NavController durch die entsprechenden Methoden von Navigator.

NavController-Feld oder -Methode

Entsprechende Navigator-Methode

navigate()

navigate()

popBackStack()

goBack()

Ersetzen Sie die Felder von NavController durch die Felder von NavigationState.

NavController-Feld oder -Methode

Entsprechende NavigationState-Methode

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Rufen Sie die Route auf oberster Ebene ab: Durchlaufen Sie die Hierarchie vom aktuellen Back-Stack-Eintrag nach oben, um sie zu finden.

topLevelRoute

Verwenden Sie NavigationState.topLevelRoute, um das Element zu 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 4.1: Lebenszyklusabhängige Logik migrieren

In Navigation 2 implementiert NavBackStackEntry LifecycleOwner, sodass Sie mit navController.currentBackStackEntry Lebenszyklusereignisse abhören oder Flows auf lebenszyklusabhängige Weise erfassen können.

In Navigation 3, NavDisplay stellt über LocalLifecycleOwner.current für jeden zusammensetzbaren Inhalt des Ziels einen LifecycleOwner mit Eintragsbereich bereit. Weitere Informationen finden Sie unter Lebenszyklus des Ziels.

Sie sollten lebenszyklusabhängige Vorgänge direkt im zusammensetzbaren Inhalt des Ziels ausführen, indem Sie auf LocalLifecycleOwner.current verweisen.

Beispiel: Sie erfassen einen Flow auf lebenszyklusabhängige Weise mit dem Back-Stack-Eintrag:

Vorher:

// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)

Nachher:

// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()

Schritt 5: Ziele von NavHosts NavGraph in einen entryProvider verschieben

In Navigation 2 definieren Sie Ihre Ziele mit der NavGraphBuilder-DSL, normalerweise im nachgestellten Lambda von NavHost. Hier werden häufig Erweiterungs funktionen verwendet, wie unter Navigationscode kapseln beschrieben.

In Navigation 3 definieren Sie Ihre Ziele mit einem entryProvider. This entryProvider löst eine Route in einen NavEntry auf. Wichtig: Der entryProvider definiert keine Über-/Untergeordnet-Beziehungen zwischen Einträgen.

In dieser Migrationsanleitung werden Über-/Untergeordnet-Beziehungen so modelliert:

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

Schritt 5.1: entryProvider erstellen

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

val entryProvider = entryProvider {

}

Schritt 5.2: Ziele in den entryProvider verschieben

Führen Sie für jedes Ziel, das in NavHost definiert ist, je nach Zieltyp die folgenden Schritte aus:

  • navigation: Löschen Sie es zusammen mit der Route. „Basisrouten“ sind nicht erforderlich, da die Routen auf oberster Ebene jeden verschachtelten Back-Stack identifizieren können.
  • composable<T>: Verschieben Sie es in entryProvider und benennen Sie es in entry um. Behalten Sie den Typparameter bei. Beispiel: composable<RouteA> wird zu entry<RouteA>.
  • dialog<T>: Führen Sie dieselben Schritte wie für composable aus, fügen Sie aber Metadaten zum Eintrag hinzu: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: Folgen Sie der Anleitung für Ansichten am unteren Rand. Diese ähnelt der Anleitung für dialog, mit dem Unterschied, 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 Graphen verwendet werden, ersetzen Sie alle Verweise auf die gelöschte Route durch den Typ, der zum Identifizieren des ersten untergeordneten Elements im verschachtelten Graphen 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. Diese Ersetzung muss normalerweise für die Liste erfolgen, die einer Navigationsleiste, einer Navigationsschiene oder einer Navigationsleiste mit Drawer bereitgestellt wird.

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

Rufen Sie Navigationsargumente mit dem Schlüssel ab, der dem nachgestellten Lambda von entry bereitgestellt 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 in die Einträge konvertiert, die von NavDisplay angezeigt werden und entryProvider verwendet werden.
  • Verknüpfen Sie NavDisplay.onBack mit navigator.goBack(). Dadurch aktualisiert navigator den Navigationsstatus, wenn der integrierte Back-Handler von NavDisplay abgeschlossen ist.
  • Wenn Sie Dialogfeldziele haben, fügen Sie DialogSceneStrategy dem Parameter sceneStrategies von NavDisplay hinzu.

Beispiel:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategies = remember { listOf(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 aufgetreten sind, melden Sie sie hier.