Android-Skills
Auf GitHub ansehenJetpack Navigation 3
android skills add navigation-3So migrieren Sie Ihre App von Navigation 2 zu Navigation 3:
- Fügen Sie die Navigation 3-Abhängigkeiten hinzu.
- Aktualisieren Sie Ihre Navigationsrouten, um die
NavKey-Schnittstelle zu implementieren. - Erstellen Sie Klassen, um den Navigationsstatus zu speichern und zu ändern.
- Ersetzen Sie
NavControllerdurch diese Klassen. - Verschieben Sie Ihre Ziele aus dem
NavGraphvonNavHostin einenentryProvider. - Ersetzen Sie
NavHostdurchNavDisplay. - Entfernen Sie die Navigation 2-Abhängigkeiten.
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
compileSdk36 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 wird sichergestellt, dass sich das Navigationsverhalten nach Abschluss der Migration nicht geändert hat.
Annahmen
In diesem Leitfaden werden die folgenden Annahmen zu Ihnen und Ihrem Projekt getroffen:
- Sie haben eine oder mehrere Routen der obersten 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 diesem Leitfaden 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 Repository mit Code-Rezepten enthält detailliertere Anwendungs fälle, darunter:
- Bottom Sheets (Anleitungen finden Sie in diesem Leitfaden)
- Modularisierter Navigationscode und eingefügte Ziele
- Argumente an
ViewModelübergeben und verwenden - Ergebnisse von einem Bildschirm zurückgeben
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
Dieser Migrationsleitfaden 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
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 wichtigsten Abhängigkeiten können Sie kopieren.
lib.versions.toml
[versions]
nav3Core = "1.1.7"
# If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library
lifecycleViewmodelNav3 = "2.11.0"
[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, 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 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 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 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 der obersten Ebene, jede mit ihrem eigenen Back-Stack. Intern wird rememberSerializable (nicht rememberSaveable) verwendet, um die aktuelle Route der obersten Ebene beizubehalten, und rememberNavBackStack, um die Back-Stacks 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 Ihren Paketnamen entsprechend Ihrer Projektstruktur hinzu.
// 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:
navigatezu einer bestimmten Route.goBackvon 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.
|
Entsprechende |
|---|---|
|
|
|
|
Ersetzen Sie die Felder von NavController durch die Felder von NavigationState.
|
Entsprechende |
|---|---|
|
|
|
|
Rufen Sie die Route der obersten Ebene ab: Durchlaufen Sie die Hierarchie vom aktuellen Back-Stack-Eintrag nach oben, um sie zu finden. |
|
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 auf Lebenszyklusereignisse warten 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 lebenszyklusabhängigen LifecycleOwner
im Bereich des Eintrags bereit. Weitere Informationen finden Sie unter Lebenszyklus von Zielen.
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 aus dem NavGraph von NavHost in einen entryProvider verschieben
In Navigation 2 definieren Sie Ihre Ziele
mit der NavGraphBuilder DSL,
normalerweise im nachfolgenden Lambda von NavHost'NavHost'. Hier werden häufig Erweiterungs
funktionen verwendet, wie unter Navigationscode kapseln beschrieben.
In Navigation 3 definieren Sie Ihre Ziele mit einem entryProvider. Dies
entryProvider löst eine Route zu einem NavEntry auf. Wichtig ist, dass der entryProvider keine Beziehungen zwischen über- und untergeordneten Einträgen definiert.
In diesem Migrationsleitfaden werden Beziehungen zwischen über- und untergeordneten Elementen so modelliert:
NavigationStatehat eine Reihe von Routen der obersten Ebene (die übergeordneten Routen) und einen Stack für jede Route. Es 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 der obersten 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<NavKey> { }
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 der obersten Ebene jeden verschachtelten Back-Stack identifizieren können.composable<T>: Verschieben Sie es inentryProviderund benennen Sie es inentryum. Behalten Sie dabei den Typparameter bei. Beispiel:composable<RouteA>wird zuentry<RouteA>.dialog<T>: Führen Sie dieselben Schritte wie fürcomposableaus, fügen Sie aber Metadaten zum Eintrag hinzu:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Folgen Sie hier dem Rezept für Ansichten am unteren Rand. Das ähnelt der Anleitung fürdialog, mit dem Unterschied, dassBottomSheetSceneStrategynicht 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 nachfolgenden 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 @Composable fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ composable<RouteA>{ entry -> 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
NavHostund ersetzen Sie es durchNavDisplay. - Geben Sie
entries = navigationState.toEntries(entryProvider)als Parameter an. Dadurch wird der Navigationsstatus in die Einträge konvertiert, die vonNavDisplayangezeigt werden undentryProviderverwendet werden. - Verbinden Sie
NavDisplay.onBackmitnavigator.goBack(). Dadurch wird der Navigationsstatus vonnavigatoraktualisiert, wenn der integrierte Back-Handler vonNavDisplayabgeschlossen ist. - Wenn Sie Dialogziel haben, fügen Sie
DialogSceneStrategydem ParametersceneStrategiesvonNavDisplayhinzu.
Beispiel:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Schritt 7: Deeplinks migrieren
In Navigation 2 wurden Deeplinks direkt im Navigationsgraphen mit dem Parameter deepLinks der Ziele definiert.
In Navigation 3 werden Deeplinks unabhängig von der Navigations-UI verwaltet. Sie definieren DeepLinkMatcher und gleichen eingehende Anfragen in Ihrer Aktivität ab, um den anfänglichen Back-Stack zu erstellen.
Vorher:
In Navigation 2 haben Sie möglicherweise einen Deeplink so definiert:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Nachher:
In Navigation 3 definieren Sie einen UriDeepLinkMatcher für die Route:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Gleichen Sie dann in onCreate (und onNewIntent) Ihrer Aktivität den eingehenden Intent ab und initialisieren Sie den Back-Stack:
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
Benutzerdefinierte Argumenttypen
In Navigation 2 wurden benutzerdefinierte oder Drittanbieter-Argumenttypen (z. B. LocalDateTime) mit benutzerdefinierten NavType-Implementierungen und typeMap verarbeitet.
In Navigation 3 definieren Sie einen DeepLinkSerializer, um benutzerdefinierte oder
Drittanbietertypen aus URI-Parametern zu deserialisieren. Weitere Informationen finden Sie unter
Benutzerdefinierte Serialisierung mit DeepLinkSerializer.
Weitere Informationen zu komplexeren Anwendungsfällen, einschließlich synthetischer Back-Stacks und benutzerdefinierter Matcher, finden Sie im Leitfaden Deeplinks unterstützen.
Schritt 8: 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 dieses Leitfadens Probleme aufgetreten sind, melden Sie einen Fehler hier.