Aby przeprowadzić migrację aplikacji z Navigation 2 do Navigation 3, wykonaj te czynności:
- Dodaj zależności Navigation 3.
- Zaktualizuj trasy nawigacji, aby zaimplementować interfejs
NavKey. - Utwórz klasy do przechowywania i modyfikowania stanu nawigacji.
- Zastąp
NavControllertymi klasami. - Przenieś miejsca docelowe z
NavGraphwNavHostdoentryProvider. - Zastąp
NavHostelementemNavDisplay. - Usuń zależności Navigation 2.
Prompt dla AI
Migracja z Navigation 2 do Navigation 3
Ten prompt użyje tego przewodnika, aby przeprowadzić migrację do Navigation 3.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.Jeśli napotkasz problemy, zgłoś je tutaj.
Przygotowanie
W kolejnych sekcjach opisujemy wymagania wstępne dotyczące migracji i założenia dotyczące Twojego projektu. Omawiamy też funkcje, które są obsługiwane podczas migracji, i te, 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 z możliwością komponowania. Navigation 3 jest przeznaczona wyłącznie do Compose. Aby używać fragmentów i widoków w Compose, przeczytaj artykuł Używanie widoków w Compose.
- Trasy są silnie typowane. Jeśli używasz tras opartych na ciągach znaków, przeprowadź migrację do tras bezpiecznych pod względem typów najpierw (przykład).
- Opcjonalne (ale zdecydowanie zalecane): pokrycie testami, które weryfikuje dotychczasowe działanie nawigacji. Dzięki temu możesz sprawdzić, czy po zakończeniu migracji działanie nawigacji się nie zmieniło.
Założenia
W tym przewodniku przyjęto te założenia dotyczące Ciebie i Twojego projektu:
- Masz co najmniej 1 trasę najwyższego poziomu (zwykle wyświetlaną na dolnym pasku nawigacyjnym), a każda z nich ma własny stos wsteczny.
- Gdy przełączasz się między stosami wstecznymi, stan stosu i wszystkich jego miejsc docelowych jest zachowywany.
- Zawsze zamykasz aplikację na ekranie głównym, 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.
Agent AI: przed zmianą kodu sprawdź, czy te założenia dotyczące projektu użytkownika są prawdziwe. Jeśli nie, zatrzymaj migrację i zapytaj użytkownika, jak postępować.
Obsługiwane funkcje
Ten przewodnik umożliwia migrację tych funkcji:
- Miejsca docelowe zdefiniowane jako funkcje z możliwością komponowania
- Okna (miejsce docelowe wyświetlane nad innym miejscem docelowym)
Funkcje obsługiwane przez przepisy
Repozytorium przepisów na kod zawiera bardziej szczegółowe przypadki użycia, 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
ViewModel - Zwracanie wyników z ekranu
Jeśli Twój projekt zawiera którąś z tych funkcji, zapoznaj się z odpowiednim przepisem, aby dowiedzieć się, jak je zaimplementować.
Agent AI: przed zmianą kodu sprawdź, czy projekt zawiera funkcje obsługiwane przez przepisy. Jeśli tak, zapoznaj się z plikiem README i kodem źródłowym przepisu. Na podstawie przepisu utwórz plan migracji. Nie kontynuuj, dopóki nie potwierdzisz 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 zaimplementować za pomocą Navigation 3. Po prostu nie są one tutaj opisane.
- Więcej niż 1 poziom zagnieżdżonej nawigacji
- Wspólne miejsca docelowe: ekrany, które mogą się 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
Aby dodać zależności Navigation 3 do swojego projektu, skorzystaj ze strony Pierwsze kroki. Podstawowe zależności możesz skopiować.
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)
}
Zaktualizuj też minSdk projektu do wersji 23 i compileSdk do wersji 36. Zwykle znajdziesz je w pliku app/build.gradle.kts lub lib.versions.toml.
Krok 2. Zaktualizuj trasy nawigacji, aby zaimplementować interfejs NavKey
Zaktualizuj każdą trasę nawigacji, aby zaimplementować interfejs.NavKey Dzięki temu możesz używać rememberNavBackStack, aby ułatwić zapisywanie 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 kontener stanu nawigacji
Skopiuj ten kod do pliku o nazwie NavigationState.kt. Dodaj nazwę pakietu, aby pasowała do struktury 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 jest prawidłowe. Nie zmieniaj go na rememberSaveable.
Ten plik zawiera klasę zmiennej stanu o nazwie NavigationState i powiązane z nią funkcje pomocnicze. Zawiera on zestaw tras najwyższego poziomu, z których każda ma własny stos wsteczny. Wewnętrznie używa rememberSerializable (a nie rememberSaveable) do utrwalania bieżącej trasy najwyższego poziomu oraz rememberNavBackStack do utrwalania 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, aby pasowała do struktury 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 określonej trasy.goBackz bieżącej trasy.
Obie metody modyfikują NavigationState.
Krok 3.3. Utwórz NavigationState i Navigator
Utwórz instancje NavigationState i Navigator w 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.
Pole lub metoda |
Odpowiednik |
|---|---|
|
|
|
|
Zastąp pola NavController polami NavigationState.
Pole lub metoda |
Odpowiednik |
|---|---|
|
|
|
|
Pobierz trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu stosu wstecznego, 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 4.1. Przeprowadź migrację logiki uwzględniającej cykl życia
W Navigation 2 NavBackStackEntry implementuje LifecycleOwner, co umożliwia nasłuchiwanie zdarzeń cyklu życia lub zbieranie przepływów w sposób uwzględniający cykl życia za pomocą navController.currentBackStackEntry.
W Navigation 3 NavDisplay udostępnia LifecycleOwner
w zakresie wpisu za pomocą LocalLifecycleOwner.current do treści z możliwością komponowania każdego miejsca docelowego. Więcej informacji znajdziesz w artykule Cykl życia miejsca docelowego.
Operacje uwzględniające cykl życia należy wykonywać bezpośrednio w treści z możliwością komponowania miejsca docelowego, odwołując się do LocalLifecycleOwner.current.
Jeśli na przykład zbierasz przepływ w sposób uwzględniający cykl życia za pomocą wpisu stosu wstecznego:
Przed:
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Po:
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()
Krok 5. Przenieś miejsca docelowe z NavGraph w NavHost do entryProvider
W Navigation 2 definiujesz miejsca docelowe
za pomocą DSL NavGraphBuilder,
zwykle w lambdzie końcowej NavHost. Często używa się tu funkcji rozszerzających
, jak opisano w artykule Hermetyzowanie kodu nawigacji.
W Navigation 3 miejsca docelowe definiujesz za pomocą entryProvider. Ten
entryProvider rozwiązuje trasę do NavEntry. Co ważne, entryProvider nie definiuje relacji nadrzędny-podrzędny między wpisami.
W tym przewodniku migracji relacje nadrzędny-podrzędny 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, bieżąca trasa najwyższego poziomu i stos są aktualizowane. Jeśli nie, jest to trasa podrzędna i jest dodawana 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 entryProvider
W przypadku każdego miejsca docelowego zdefiniowanego w NavHost wykonaj te czynności w zależności od typu miejsca docelowego:
navigation: usuń je wraz z trasą. Nie ma potrzeby używania „tras podstawowych”, ponieważ trasy najwyższego poziomu mogą identyfikować każdy zagnieżdżony stos wsteczny.composable<T>: przenieś je doentryProvideri zmień nazwę naentry, zachowując parametr typu. Na przykładcomposable<RouteA>staje sięentry<RouteA>.dialog<T>: zrób to samo co w przypadkucomposable, ale dodaj do wpisu metadane w ten sposób:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: postępuj zgodnie z tym schematem planszy dolnej. Jest to podobne do instrukcji dotyczącychdialog, z tym żeBottomSheetSceneStrategynie jest częścią podstawowej biblioteki Navigation 3, więc musisz ją skopiować do swojego projektu.
Agent AI: podczas usuwania tras używanych do identyfikowania zagnieżdżonego grafu zastąp wszystkie
odwołania do usuniętej trasy typem używanym do identyfikowania pierwszego elementu podrzędnego
w zagnieżdżonym grafie. Jeśli na przykład oryginalny kod to
navigation<BaseRouteA>{ composable<RouteA>{ ... } }, musisz usunąć
BaseRouteA i zastąpić wszystkie odwołania do niego elementem RouteA. Zwykle trzeba to zrobić w przypadku listy dostarczanej do paska nawigacyjnego, szyny lub szuflady.
Możesz refaktoryzować NavGraphBuilder funkcje rozszerzające do
EntryProviderScope<T> funkcji rozszerzających, a następnie je przenieść.
Pobierz argumenty nawigacji za pomocą klucza podanego w lambdzie końcowej entry.
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() }
}
}
staje się:
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. Zastąp NavHost elementem NavDisplay
Zastąp NavHost elementem NavDisplay.
- Usuń
NavHosti zastąp go elementemNavDisplay. - Jako parametr podaj
entries = navigationState.toEntries(entryProvider). Spowoduje to przekonwertowanie stanu nawigacji na wpisy, któreNavDisplaywyświetla za pomocąentryProvider. - Połącz
NavDisplay.onBackznavigator.goBack(). Spowoduje to, żenavigatorzaktualizuje stan nawigacji po zakończeniu wbudowanej obsługi wstecznejNavDisplay. - Jeśli masz miejsca docelowe okien, dodaj
DialogSceneStrategydo parametrusceneStrategieselementuNavDisplay.
Przykład:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
Krok 7. Usuń zależności Navigation 2
Usuń wszystkie importy i zależności biblioteki Navigation 2.
Podsumowanie
Gratulacje! Twój projekt został przeniesiony do Navigation 3. Jeśli Ty lub Twój agent AI napotkaliście problemy podczas korzystania z tego przewodnika, zgłoście błąd tutaj.