Umiejętności związane z Androidem
Wyświetl na GitHubJetpack Navigation 3
android skills add navigation-3Aby 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.
Przygotowanie
W kolejnych sekcjach opisujemy wymagania wstępne dotyczące migracji i założenia dotyczące 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. - Powinieneś(-aś) znać terminologię związaną z nawigacją.
- Miejsca docelowe to funkcje kompozycyjne. 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).
- Opcjonalnie (ale zdecydowanie zalecane): pokrycie testami, które weryfikuje dotychczasowe działanie nawigacji. Dzięki temu sprawdzisz, 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 nawigacji), 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 wychodzisz z aplikacji przez ekran Główny, 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 kompozycyjne
- Okna dialogowe (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:
- Arkusze dolne (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 ją 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ą tu 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 są dostępne do skopiowania.
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)
}
Zaktualizuj też minSdk projektu do 23 i compileSdk do 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ć NavKeyinterfejs. 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 |
|---|---|
|
|
|
|
Aby uzyskać trasę najwyższego poziomu, przejdź w górę hierarchii od bieżącego wpisu stosu wstecznego. |
|
Użyj NavigationState.topLevelRoute, aby określić element, który jest obecnie wybrany na pasku nawigacji.
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 kompozycyjnej 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 kompozycyjnej 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 Hermetyzacja 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 (tras nadrzędnych) 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<NavKey> { }
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 metadane do wpisu w ten sposób:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: postępuj zgodnie z tym przepisem na planszę dolną. Jest to podobne do instrukcji dotyczącychdialog, z tym żeBottomSheetSceneStrategynie jest częścią podstawowej biblioteki Navigation 3, więc musisz ją skopiować do 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, kolumny bocznej lub panelu.
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.
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 @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() } } }
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 dialogowych, dodaj
DialogSceneStrategydo parametrusceneStrategieswNavDisplay.
Na przykład:
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ś błąd tutaj.