Migracja z nawigacji 2 na nawigację 3

Aby przenieść aplikację z Navigation 2 do Navigation 3, wykonaj te czynności:

  1. Dodaj zależności Navigation 3.
  2. Zaktualizuj trasy nawigacji, aby wdrożyć interfejs NavKey.
  3. Utwórz klasy do przechowywania i modyfikowania stanu nawigacji.
  4. Zastąp NavController tymi klasami.
  5. Przenieś miejsca docelowe z NavHostNavGraph do entryProvider.
  6. Zastąp NavHost tekstem NavDisplay.
  7. 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ć compileSdk w 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:

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:

  • navigate do konkretnej trasy.
  • goBack od bieżącej trasy.

Obie metody modyfikują NavigationState.

Krok 3.3. Utwórz NavigationStateNavigator

Utwórz instancje NavigationStateNavigator 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.

NavController pole lub metoda

Navigatorrównoważnik

navigate()

navigate()

popBackStack()

goBack()

Zastąp pola NavController polami NavigationState.

Pole lub metoda NavController

NavigationStaterównoważnik

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Pobierz trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu na liście wstecz, aby ją znaleźć.

topLevelRoute

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:

  • NavigationState ma 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 Navigator sprawdza, 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 do entryProvider i zmień jego nazwę na entry, zachowując parametr typu. Na przykład composable<RouteA> zmieni się na entry<RouteA>.
  • dialog<T>: Wykonaj te same czynności co w przypadku composable, 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ących dialog, z tym że BottomSheetSceneStrategy nie 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ń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ń NavHost i zastąp go NavDisplay.
  • Określ entries = navigationState.toEntries(entryProvider) jako parametr. Spowoduje to przekształcenie stanu nawigacji w elementy, które NavDisplay wyświetla za pomocą entryProvider.
  • Połącz NavDisplay.onBack z navigator.goBack(). Spowoduje to zaktualizowanie stanu nawigacji przez navigator po zakończeniu działania wbudowanego modułu obsługi przycisku Wstecz w NavDisplay.
  • Jeśli masz miejsca docelowe dialogu, dodaj DialogSceneStrategy do parametru NavDisplaysceneStrategy.

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.