Umiejętności związane z Androidem
Wyświetl w GitHubieJetpack 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 wdrożyć interfejs
NavKey. - Utwórz klasy, aby przechowywać i modyfikować stan nawigacji.
- Zastąp
NavControllertymi klasami. - Przenieś miejsca docelowe z
NavHostwNavGraphdoentryProvider. - Zastąp
NavHosttekstemNavDisplay. - Usuń zależności Navigation 2.
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ą.
- Usługi 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ą silnie 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 sposób nawigacji nie uległ zmianie.
Założenia
W tym przewodniku przyjęto następujące założenia dotyczące Ciebie i Twojego projektu:
- Masz jedną lub kilka tras najwyższego poziomu (zwykle wyświetlanych 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.
- Aplikację zawsze zamykasz na ekranie Główna, który jest pierwszym ekranem wyświetlanym po uruchomieniu aplikacji.
- Chcesz przeprowadzić migrację z Navigation 2 na Navigation 3 w ramach jednej, niepodzielnej zmiany. Nie przeprowadzasz migracji przyrostowej, w której kod Nawigacji 2 jest używany razem z kodem Nawigacji 3.
Agent AI: 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, jak ma postępować.
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 i przekazywanie argumentów do funkcji
ViewModel - Wyświetlanie wyników z ekranu
Jeśli Twój projekt ma 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. Po prostu nie omawiamy ich tutaj.
- Więcej niż 1 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
Agent AI: przed zmianą kodu sprawdź, czy projekt zawiera któreś 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.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)
}
Zmień też wartość minSdk w projekcie na 23, a wartość 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 obiekt przechowujący stan 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 to prawidłowa odpowiedź. Nie zmieniaj go na
rememberSaveable.
Ten plik zawiera klasę przechowującą stan o nazwie NavigationState i powiązane 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, 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 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.
Pole lub metoda |
|
|---|---|
|
|
|
|
|
|
Zastąp pola NavController polami NavigationState.
|
|
|---|---|
|
|
|
|
|
|
|
|
Uzyskaj trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu na stosie wstecznym, 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. Przenieś logikę uwzględniającą cykl życia
W Navigation 2 interfejs NavBackStackEntry implementuje interfejs LifecycleOwner, co umożliwia nasłuchiwanie zdarzeń cyklu życia lub zbieranie przepływów w sposób uwzględniający cykl życia za pomocą interfejsu navController.currentBackStackEntry.
W Navigation 3 NavDisplay udostępnia LifecycleOwner
w zakresie wpisu LocalLifecycleOwner.current do treści z możliwością komponowania w każdym miejscu docelowym. Więcej informacji znajdziesz w sekcji Cykl życia miejsca docelowego.
Operacje uwzględniające cykl życia należy wykonywać bezpośrednio w treściach kompozycyjnych 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 na liście wstecznej:
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 4.2. Przenoszenie przekazywania wyników
W Nawigacji 2 miejsca docelowe przekazywały wyniki do poprzednich miejsc docelowych za pomocą funkcji SavedStateHandle na NavBackStackEntry. Ponieważ SavedStateHandle jest wspierany przez zapisany stan instancji, zwrócone dane automatycznie przetrwały śmierć procesu.
Przed:
// 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) }
W Nawigacji 3 dodaj rememberResultEventBusNavEntryDecorator() do NavDisplay.entryDecorators. W mapowaniu miejsca docelowego nadawcy entryProviderpobierz LocalResultEventBus.current i wywołaj sendResult(). W miejscu docelowym użyj ResultEffect, aby przekazać zdarzenie do ViewModel lub wywołać efekt uboczny.
Po:
// 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) }
W przypadku obserwacji opartej na stanie możesz wywołać funkcję resultBus.conflateAsState(). Więcej informacji znajdziesz w sekcji Zwracanie wyników.
Krok 5. Przenieś miejsca docelowe z NavHost w NavGraph do entryProvider
W Navigation 2 definiujesz miejsca docelowe za pomocą NavGraphBuilder DSL, zwykle w funkcji lambda końcowej NavHost. Często używa się tu funkcji rozszerzających, jak opisano w artykule Enkapsulacja kodu nawigacji.
W Navigation 3 miejsca docelowe określa się za pomocą elementu 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 między reklamodawcami nadrzędnymi a podrzędnymi 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ązaną z nią listę.- 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<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ń 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 ten sposób:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: postępuj zgodnie z instrukcjami podanymi w tym artykule. Jest to podobne do instrukcji dotyczącychdialog, z tym wyjątkiem, ż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
szuflady.
Możesz refaktoryzować NavGraphBuilder funkcje rozszerzeń do EntryProviderScope<T> funkcji rozszerzeń, a następnie je przenieść.
Pobierz argumenty nawigacji za pomocą klucza przekazanego do lambdy końcowej funkcji 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 @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() } } }
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. Zastąp NavHost tekstem 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 parametruNavDisplaysceneStrategies.
Przykład:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Krok 7. Przenieś precyzyjne linki
W Navigation 2 precyzyjne linki były definiowane bezpośrednio w wykresie nawigacji za pomocą parametru deepLinks miejsc docelowych.
W Navigation 3 precyzyjne linki są zarządzane niezależnie od interfejsu nawigacji. Określasz DeepLinkMatcher i dopasowujesz przychodzące żądania w aktywności, aby utworzyć początkowy stos wsteczny.
Przed:
W Navigation 2 możesz zdefiniować precyzyjny link w ten sposób:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Po:
W Nawigacji 3 możesz zdefiniować UriDeepLinkMatcher dla trasy:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Następnie w aktywności onCreate (i onNewIntent) dopasowujesz przychodzący zamiar i inicjujesz stos wsteczny:
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 } }
Niestandardowe typy argumentów
W Navigation 2 argumenty niestandardowe lub argumenty firm zewnętrznych (np.LocalDateTime) były obsługiwane za pomocą niestandardowych implementacji NavType i typeMap.
W Navigation 3 zdefiniuj DeepLinkSerializer, aby deserializować typy niestandardowe lub typy innych firm z parametrów URI. Więcej informacji znajdziesz w sekcji Niestandardowa serializacja za pomocą DeepLinkSerializer.
W przypadku bardziej zaawansowanych zastosowań, w tym syntetycznych list wstecznych i niestandardowych dopasowań, zapoznaj się z przewodnikiem Obsługa precyzyjnych linków.
Krok 8. 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.