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 wdrożyć interfejs NavKey.
  3. Utwórz klasy, aby przechowywać i modyfikować stan 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.

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ą.
  • 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:

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:

  • 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.

Pole lub metoda NavController

Navigatorrównoważne

navigate()

navigate()

popBackStack()

goBack()

previousBackStackEntry.savedStateHandle.set()

ResultEventBus.sendResult()

Zastąp pola NavController polami NavigationState.

NavController pole lub metoda

NavigationStaterównoważne

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

currentBackStackEntry.savedStateHandle.getLiveData()

currentBackStackEntry.savedStateHandle.getStateFlow()

ResultEffect (na podstawie zdarzeń)

ResultEventBus.conflateAsState() (zależne od stanu)

Uzyskaj trasę najwyższego poziomu: przejdź w górę hierarchii od bieżącego wpisu na stosie wstecznym, 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. 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 NavHostNavGraph 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:

  • 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ązaną z nią listę.
  • 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<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 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 ten sposób: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: postępuj zgodnie z instrukcjami podanymi w tym artykule. Jest to podobne do instrukcji dotyczących dialog, z tym wyjątkiem, ż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 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ń 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.onBacknavigator.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 NavDisplaysceneStrategies.

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:

Po:

W Nawigacji 3 możesz zdefiniować UriDeepLinkMatcher dla trasy:

Następnie w aktywności onCreate (i onNewIntent) dopasowujesz przychodzący zamiar i inicjujesz stos wsteczny:

Niestandardowe typy argumentów

W Navigation 2 argumenty niestandardowe lub argumenty firm zewnętrznych (np.LocalDateTime) były obsługiwane za pomocą niestandardowych implementacji NavTypetypeMap.

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.