Migracja z nawigacji 2 na nawigację 3

Aby przeprowadzić migrację aplikacji z Navigation 2 do Navigation 3, wykonaj te czynności:

  1. Dodaj zależności Navigation 3.
  2. Zaktualizuj trasy nawigacji, aby zaimplementować interfejs NavKey.
  3. Utwórz klasy do przechowywania i modyfikowania stanu nawigacji.
  4. Zastąp NavController tymi klasami.
  5. Przenieś miejsca docelowe z NavGraph w NavHost do entryProvider.
  6. Zastąp NavHost elementem NavDisplay.
  7. Usuń zależności Navigation 2.

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.

Korzystanie z promptów AI

Prompty AI są przeznaczone do używania w Gemini w Android Studio.

Więcej informacji o Gemini w Studio znajdziesz tutaj: https://developer.android.com/studio/gemini/overview

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ć compileSdk w 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:

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:

  • navigate do określonej trasy.
  • goBack z 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 NavController

Odpowiednik Navigator

navigate()

navigate()

popBackStack()

goBack()

Zastąp pola NavController polami NavigationState.

Pole lub metoda NavController

Odpowiednik NavigationState

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Pobierz trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu stosu wstecznego, 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 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:

  • 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, 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 do entryProvider i zmień nazwę na entry, zachowując parametr typu. Na przykład composable<RouteA> staje się entry<RouteA>.
  • dialog<T>: zrób to samo co w przypadku composable, 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ących dialog, z tym że BottomSheetSceneStrategy nie 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ń NavHost i zastąp go elementem NavDisplay.
  • Jako parametr podaj entries = navigationState.toEntries(entryProvider). Spowoduje to przekonwertowanie stanu nawigacji na wpisy, które NavDisplay wyświetla za pomocą entryProvider.
  • Połącz NavDisplay.onBack z navigator.goBack(). Spowoduje to, że navigator zaktualizuje stan nawigacji po zakończeniu wbudowanej obsługi wstecznej NavDisplay.
  • Jeśli masz miejsca docelowe okien, dodaj DialogSceneStrategy do parametru sceneStrategies elementu NavDisplay.

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.