Android-Kenntnisse
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
NavHostsNavGraphin einentryProvider. - Ersetzen Sie
NavHostdurchNavDisplay. - Entfernen Sie die Abhängigkeiten von Navigation 2.
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.
Vorbereitung
- Sie müssen mindestens eine
compileSdkvon 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 Views in Compose finden Sie unter Views 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 bezüglich Ihnen und Ihrem Projekt ausgegangen:
- Sie haben eine oder mehrere Routen auf oberster Ebene (die in der Regel 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-Agent: Prüfen Sie vor dem Ändern von Code, ob diese Annahmen zum Projekt des Nutzers zutreffen. Wenn nicht, stoppe die Migration und frage den Nutzer, wie du vorgehen sollst.
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:
- Bottom Sheets (Anleitung in diesem Leitfaden)
- Modularisierter Navigationscode und eingefügte Ziele
- Argumente für
ViewModelverwenden und übergeben - Ergebnisse von einem Bildschirm zurückgeben
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 in der README-Datei und im Quellcode des Rezepts nach. Erstellen Sie einen Migrationsplan auf Grundlage des Rezepts. Fahre erst fort, wenn du den Plan mit dem Nutzer bestätigt hast.
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
KI-Agent: Prüfen Sie vor dem Ändern von Code, ob das Projekt eine der nicht unterstützten Funktionen enthält. Wenn das der Fall ist, 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 Get started (Erste Schritte) die Navigation 3-Abhängigkeiten zu Ihrem Projekt hinzu. Die wichtigsten Abhängigkeiten werden zum Kopieren bereitgestellt.
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 auch minSdk des Projekts auf 23 und compileSdk auf 36. Sie finden diese in der Regel unter app/build.gradle.kts oder lib.versions.toml.
Schritt 2: Navigationsrouten aktualisieren, um die NavKey-Schnittstelle zu implementieren
Aktualisieren Sie jede Route für die Navigation, 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 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, 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 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.
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 |
|
|---|---|
|
|
|
|
|
|
Ersetzen Sie NavController-Felder durch NavigationState-Felder.
Feld oder Methode |
|
|---|---|
|
|
|
|
|
|
|
|
Die Route der obersten Ebene abrufen: Durchlaufe die Hierarchie vom aktuellen Backstack-Eintrag aus nach oben, um sie zu finden. |
|
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 4.1: Lebenszyklusbewusste Logik migrieren
In Navigation 2 wird NavBackStackEntry LifecycleOwner implementiert, sodass Sie mit navController.currentBackStackEntry
auf Lifecycle-Ereignisse warten oder Flows auf Lifecycle-bewusste Weise erfassen können.
In Navigation 3 bietet NavDisplay über LocalLifecycleOwner.current einen LifecycleOwner auf Eintragsebene für den zusammensetzbaren Inhalt jedes Ziels. Weitere Informationen finden Sie unter Ziel-Lebenszyklus.
Sie sollten lebenszyklusbezogene Vorgänge direkt im zusammensetzbaren Inhalt Ihres Ziels ausführen, indem Sie auf LocalLifecycleOwner.current verweisen.
Wenn Sie beispielsweise einen Ablauf auf lebenszyklusbewusste Weise mithilfe des Backstack-Eintrags erfassen:
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 4.2: Übergabe von Migrationsergebnissen
In Navigation 2 wurden Ergebnisse mithilfe von SavedStateHandle auf NavBackStackEntry an vorherige Ziele zurückgegeben. Da SavedStateHandle durch den gespeicherten Instanzstatus gesichert wird, werden zurückgegebene Daten automatisch nach dem Beenden des Prozesses beibehalten.
Vorher:
// Sender destination: navController.previousBackStackEntry?.savedStateHandle?.set("contact_key", contact) navController.popBackStack() // Receiver destination: val lifecycleOwner = LocalLifecycleOwner.current navController.currentBackStackEntry?.savedStateHandle ?.getLiveData<Contact>("contact_key") ?.observe(lifecycleOwner) { contact -> viewModel.onRecipientSelected(contact) }
Fügen Sie in Navigation 3 rememberResultEventBusNavEntryDecorator() zu Ihrer NavDisplay.entryDecorators hinzu. Rufen Sie in der entryProvider-Zuordnung des Absenderziels LocalResultEventBus.current ab und rufen Sie sendResult() auf. Verwenden Sie im Empfangsziel ResultEffect, um das Ereignis an ein ViewModel weiterzuleiten oder einen Nebeneffekt auszulösen.
Nachher:
// Sender destination (in entryProvider): entry<ContactPickerRoute> { val resultBus = LocalResultEventBus.current ContactPickerScreen( onContactSelected = { contact -> resultBus.sendResult<Contact>(result = contact) navigator.goBack() } ) } // Receiver destination: @Composable fun ComposeMessageScreen(viewModel: ComposeMessageViewModel = viewModel()) { ResultEffect<Contact> { contact -> viewModel.onRecipientSelected(contact) } ComposeMessageContent(recipient = viewModel.recipient) }
Für die statusbasierte Beobachtung können Sie resultBus.conflateAsState() aufrufen. Weitere Informationen finden Sie unter Ergebnisse zurückgeben.
Schritt 5: Ziele aus NavHosts NavGraph in ein entryProvider verschieben
In Navigation 2 definieren Sie Ihre Ziele mit der NavGraphBuilder-DSL, normalerweise in der nachgestellten Lambda-Funktion von NavHost. Hier werden häufig Erweiterungsfunktionen verwendet, 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:
NavigationStatehat eine Reihe von Routen auf oberster Ebene (die übergeordneten Routen) und einen Stapel für jede Route. Sie verfolgt die aktuelle Route der obersten Ebene und den zugehörigen Stapel.- 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<NavKey> { }
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. „base routes“ ist nicht erforderlich, da die Routen der obersten Ebene jeden verschachtelten Backstack identifizieren können.composable<T>: Verschieben Sie sie inentryProviderund benennen Sie sie inentryum. Der Typparameter wird beibehalten. Auscomposable<RouteA>wird beispielsweiseentry<RouteA>.dialog<T>: Führen Sie dieselben Schritte wie beicomposableaus, fügen Sie dem Eintrag aber Metadaten hinzu:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Folge dem Rezept in der Ansicht am unteren Rand. Das Vorgehen ist ähnlich wie beidialog, mit dem Unterschied, dassBottomSheetSceneStrategynicht Teil der Navigation 3-Kernbibliothek ist. Sie sollten es 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. Dieser Ersatz muss in der Regel für die Liste erfolgen, die für eine Navigationsleiste, eine Seitenleiste oder ein 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 @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 mithilfe vonentryProviderin die Einträge umgewandelt, die inNavDisplayangezeigt werden. NavDisplay.onBackverbinden mitnavigator.goBack(). Dadurch wird der Navigationsstatus vonnavigatoraktualisiert, wenn der integrierte Back-Handler vonNavDisplayabgeschlossen ist.- Wenn Sie Dialogziele 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 Navigationsdiagramm mit dem Parameter deepLinks von Zielen 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 ursprünglichen Backstack 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 eine UriDeepLinkMatcher für die Route:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
In der Methode onCreate (und onNewIntent) Ihrer Aktivität gleichen Sie dann den eingehenden Intent ab und initialisieren den Backstack:
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 haben Sie benutzerdefinierte oder Drittanbieter-Argumenttypen (z. B. LocalDateTime) mit benutzerdefinierten NavType-Implementierungen und typeMap behandelt.
Definieren Sie in Navigation 3 ein DeepLinkSerializer, um benutzerdefinierte oder Drittanbietertypen aus URI-Parametern zu deserialisieren. Weitere Informationen finden Sie unter Benutzerdefinierte Serialisierung mit DeepLinkSerializer.
Informationen zu komplexeren Anwendungsfällen, einschließlich synthetischer Backstacks 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 Sie oder Ihr KI-Agent bei der Verwendung dieser Anleitung auf Probleme stoßen, melden Sie hier einen Fehler.