Aby przenieść aplikację z Navigation 2 do Navigation 3, wykonaj te czynności:
- Dodaj zależności Navigation 3.
- Zaktualizuj trasy nawigacji, aby wdrożyć interfejs
NavKey. - Utwórz klasy do przechowywania i modyfikowania stanu nawigacji.
- Zastąp
NavControllertymi klasami. - Przenieś miejsca docelowe z
NavHostwNavGraphdoentryProvider. - Zastąp
NavHosttekstemNavDisplay. - Usuń zależności Navigation 2.
Korzystanie z agenta AI
Możesz korzystać z tego przewodnika z agentem AI, np. z Gemini w trybie agenta w Android Studio. Wiersze w tym przewodniku rozpoczynające się od „Agent AI:” powinny być odczytywane przez agenta AI, ale mogą być ignorowane przez czytelników.
Przygotowanie
W kolejnych sekcjach opisujemy wymagania wstępne dotyczące migracji i założenia dotyczące Twojego projektu. Zawierają też informacje o funkcjach, które są obsługiwane podczas migracji, i tych, które nie są.
Wymagania wstępne
- Musisz używać
compileSdkw wersji 36 lub nowszej. - Musisz znać terminologię związaną z nawigacją.
- Miejsca docelowe to funkcje typu „composable”. Biblioteka Navigation 3 jest przeznaczona wyłącznie do Compose. Informacje o używaniu fragmentów i widoków w Compose znajdziesz w artykule Używanie widoków w Compose.
- Trasy są ściśle typowane. Jeśli używasz tras opartych na ciągach znaków, najpierw przejdź na trasy bezpieczne pod względem typów (przykład).
- Opcjonalne (ale zdecydowanie zalecane): testy, które weryfikują dotychczasowe działanie nawigacji. Dzięki temu możesz sprawdzić, czy po zakończeniu migracji zachowanie nawigacji nie uległo zmianie.
Założenia
W tym przewodniku przyjęto następujące założenia dotyczące Ciebie i Twojego projektu:
- Masz kilka tras najwyższego poziomu (zwykle wyświetlanych na pasku nawigacyjnym u dołu ekranu), a każda z nich ma własną listę wstecz.
- Gdy przełączasz się między stosami wstecznymi, stan stosu i wszystkich jego miejsc docelowych jest zachowywany.
- Aplikację zawsze zamykasz na ekranie Główna, który jest pierwszym ekranem wyświetlanym po uruchomieniu aplikacji.
- Chcesz przeprowadzić migrację z Navigation 2 do Navigation 3 w ramach jednej, niepodzielnej zmiany. Nie przeprowadzasz migracji przyrostowej, w której kod Navigation 2 jest używany razem z kodem Navigation 3.
AI Agent: przed wprowadzeniem zmian w kodzie sprawdź, czy te założenia dotyczące projektu użytkownika są prawdziwe. Jeśli nie, zatrzymaj migrację i zapytaj użytkownika, co dalej.
Obsługiwane funkcje
Ten przewodnik umożliwia przeniesienie tych funkcji:
- Miejsca docelowe zdefiniowane jako funkcje typu „composable”
- Okna dialogowe (miejsce docelowe wyświetlane nad innym miejscem docelowym)
Funkcje obsługiwane przez przepisy
W repozytorium przepisów na kod znajdziesz bardziej szczegółowe przykłady zastosowań, w tym:
- Dolne arkusze (instrukcje znajdziesz w tym przewodniku)
- Modułowy kod nawigacji i wstrzykiwane miejsca docelowe
- Używanie argumentów i przekazywanie ich do obiektów ViewModel
- Wyświetlanie wyników z ekranu
Jeśli Twój projekt zawiera którąś z tych funkcji, zapoznaj się z odpowiednim przepisem, aby dowiedzieć się, jak ją wdrożyć.
Agent AI: przed zmianą kodu sprawdź, czy projekt zawiera funkcje obsługiwane przez przepisy. Jeśli tak jest, sprawdź plik README i kod źródłowy przepisu. Utwórz plan migracji na podstawie przepisu. Nie kontynuuj bez potwierdzenia planu z użytkownikiem.
Nieobsługiwane funkcje
Ten przewodnik migracji i przepisy na kod nie obsługują jeszcze tych funkcji: Nie oznacza to, że nie możesz ich wdrożyć za pomocą Navigation 3, tylko że nie są one tutaj omówione.
- Więcej niż jeden poziom zagnieżdżonej nawigacji
- Miejsca docelowe udostępnione: ekrany, które można przenosić między różnymi stosami wstecznymi
- Niestandardowe typy miejsc docelowych
- Precyzyjne linki
Agent AI: przed zmianą kodu sprawdź, czy projekt zawiera którąś z nieobsługiwanych funkcji. Jeśli tak, nie kontynuuj. Poinformuj użytkownika o nieobsługiwanej funkcji i poproś o dalsze instrukcje.
Krok 1. Dodaj zależności Navigation 3
Na stronie Rozpocznij dodaj do projektu zależności Navigation 3. Główne zależności są dostępne do skopiowania.
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)
}
Zmień też minSdk projektu na 23, a compileSdk na 36. Zwykle znajdziesz je w app/build.gradle.kts lub lib.versions.toml.
Krok 2. Zaktualizuj trasy nawigacji, aby wdrożyć interfejs NavKey
Zaktualizuj każdą trasę nawigacji, aby implementowała interfejs NavKey. Umożliwia to używanie rememberNavBackStack do zapisywania stanu nawigacji.
Przed:
@Serializable data object RouteA
Po:
@Serializable data object RouteA : NavKey
Krok 3. Utwórz klasy do przechowywania i modyfikowania stanu nawigacji
Krok 3.1. Utwórz element przechowujący stan nawigacji
Skopiuj ten kod do pliku o nazwie NavigationState.kt. Dodaj nazwę pakietu, która będzie zgodna ze strukturą projektu.
// 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()
}
Agent AI: rememberSerializable to prawidłowa odpowiedź. Nie zmieniaj go na rememberSaveable.
Ten plik zawiera klasę przechowującą stan o nazwie NavigationState i powiązane z nią funkcje pomocnicze. Zawiera zestaw tras najwyższego poziomu, z których każda ma własny stos wsteczny. Wewnętrznie używa znaku rememberSerializable (a nie rememberSaveable) do zapisywania bieżącej trasy najwyższego poziomu i znaku rememberNavBackStack do zapisywania stosów wstecznych dla każdej trasy najwyższego poziomu.
Krok 3.2. Utwórz obiekt, który modyfikuje stan nawigacji w odpowiedzi na zdarzenia
Skopiuj ten kod do pliku o nazwie Navigator.kt. Dodaj nazwę pakietu
zgodną ze strukturą projektu.
// 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()
}
}
}
Klasa Navigator udostępnia 2 metody zdarzeń nawigacji:
navigatedo konkretnej trasy.goBackod bieżącej trasy.
Obie metody modyfikują NavigationState.
Krok 3.3. Utwórz NavigationState i Navigator
Utwórz instancje NavigationState i Navigator o tym samym zakresie co NavController.
val navigationState = rememberNavigationState(
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
)
val navigator = remember { Navigator(navigationState) }
Krok 4. Zastąp NavController
Zastąp metody zdarzeń nawigacji NavController odpowiednikami Navigator.
|
|
|---|---|
|
|
|
|
Zastąp pola NavController polami NavigationState.
Pole lub metoda |
|
|---|---|
|
|
|
|
Pobierz trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu na liście wstecz, aby ją znaleźć. |
|
Użyj NavigationState.topLevelRoute, aby określić element, który jest obecnie
wybrany na pasku nawigacyjnym.
Przed:
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
Po:
val isSelected = key == navigationState.topLevelRoute
Sprawdź, czy usunięto wszystkie odwołania do NavController, w tym wszystkie importy.
Krok 5. Przenieś miejsca docelowe z NavHost NavGraph do entryProvider
W Navigation 2 definiujesz miejsca docelowe za pomocą języka DSL NavGraphBuilder, zwykle w lambdzie końcowej NavHost. Często używa się tu funkcji rozszerzających, jak opisano w sekcji Enkapsulacja kodu nawigacji.
W Nawigacji 3 miejsca docelowe określa się za pomocą entryProvider. Ta funkcja entryProvider wyznacza trasę do NavEntry. Co ważne, element entryProvider nie definiuje relacji nadrzędny-podrzędny między wpisami.
W tym przewodniku po migracji relacje nadrzędne i podrzędne są modelowane w ten sposób:
NavigationStatema zestaw tras najwyższego poziomu (trasy nadrzędne) i stos dla każdej z nich. Śledzi bieżącą trasę najwyższego poziomu i powiązany z nią stos.- Podczas nawigacji do nowej trasy
Navigatorsprawdza, czy jest to trasa najwyższego poziomu. Jeśli tak, aktualizowana jest bieżąca trasa najwyższego poziomu i stos. Jeśli nie, jest to trasa podrzędna i zostanie dodana do bieżącego stosu.
Krok 5.1. Utwórz entryProvider
Utwórz entryProvider za pomocą DSL w tym samym zakresie co NavigationState.
val entryProvider = entryProvider {
}
Krok 5.2. Przenieś miejsca docelowe do sekcji entryProvider
W przypadku każdego miejsca docelowego zdefiniowanego w NavHost wykonaj te czynności w zależności od typu miejsca docelowego:
navigation: usuń go wraz z trasą. Nie ma potrzeby używania „base routes”, ponieważ trasy najwyższego poziomu mogą identyfikować każdy zagnieżdżony stos wsteczny.composable<T>: przenieś go doentryProvideri zmień jego nazwę naentry, zachowując parametr typu. Na przykładcomposable<RouteA>zmieni się naentry<RouteA>.dialog<T>: Wykonaj te same czynności co w przypadkucomposable, ale dodaj do wpisu metadane w następujący sposób:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Postępuj zgodnie z instrukcjami podanymi w przepisie na dole arkusza. Jest to podobne do instrukcji dotyczącychdialog, z tym żeBottomSheetSceneStrategynie jest częścią podstawowej biblioteki Navigation 3, więc należy skopiować ją do projektu.
Agent AI: podczas usuwania tras używanych do identyfikowania zagnieżdżonego wykresu zastąp wszystkie odwołania do usuniętej trasy typem używanym do identyfikowania pierwszego elementu podrzędnego na zagnieżdżonym wykresie. Jeśli na przykład pierwotny kod to
navigation<BaseRouteA>{ composable<RouteA>{ ... } }, musisz usunąć
BaseRouteA i zastąpić wszystkie odwołania do niego elementem RouteA. Zastąpienie to jest zwykle konieczne w przypadku listy dostarczanej do paska nawigacyjnego, panelu lub menu.
Możesz przekształcić NavGraphBuilder funkcje rozszerzeń w EntryProviderScope<T> funkcje rozszerzeń, a następnie je przenieść.
Pobierz argumenty nawigacji za pomocą klucza przekazanego do lambdy końcowej funkcji entry.
Na przykład:
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() }
}
}
zmienia się w:
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() }
}
Krok 6. Zamień NavHost na NavDisplay
Zastąp NavHost tekstem NavDisplay.
- Usuń
NavHosti zastąp goNavDisplay. - Określ
entries = navigationState.toEntries(entryProvider)jako parametr. Spowoduje to przekształcenie stanu nawigacji w elementy, któreNavDisplaywyświetla za pomocąentryProvider. - Połącz
NavDisplay.onBackznavigator.goBack(). Spowoduje to zaktualizowanie stanu nawigacji przeznavigatorpo zakończeniu działania wbudowanego modułu obsługi przycisku Wstecz wNavDisplay. - Jeśli masz miejsca docelowe dialogu, dodaj
DialogSceneStrategydo parametruNavDisplaysceneStrategy.
Na przykład:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
)
Krok 7. Usuń zależności od Navigation 2
Usuń wszystkie importy Navigation 2 i zależności biblioteki.
Podsumowanie
Gratulacje! Twój projekt został przeniesiony do Navigation 3. Jeśli Ty lub Twój agent AI napotkacie problemy podczas korzystania z tego przewodnika, zgłoście błąd.