Eseguire la migrazione da Navigation 2 a Navigation 3

Per eseguire la migrazione dell'app da Navigazione 2 a Navigazione 3, segui questi passaggi:

  1. Aggiungi le dipendenze di Navigation 3.
  2. Aggiorna i percorsi di navigazione per implementare l'interfaccia NavKey.
  3. Crea classi per contenere e modificare lo stato di navigazione.
  4. Sostituisci NavController con queste classi.
  5. Sposta le destinazioni da NavHost's NavGraph in un entryProvider.
  6. Sostituisci NavHost con NavDisplay.
  7. Rimuovi le dipendenze di Navigazione 2.

Utilizzare un agente AI

Puoi utilizzare questa guida con un agente AI, ad esempio Gemini in modalità Agent di Android Studio. Le righe di questa guida che iniziano con "Agente AI:" devono essere lette dall'agente AI, ma possono essere ignorate dai lettori umani.

Preparazione

Le sezioni seguenti descrivono i prerequisiti per la migrazione e le ipotesi sul tuo progetto. Inoltre, vengono illustrate le funzionalità supportate per la migrazione e quelle non supportate.

Prerequisiti

  • Devi utilizzare un compileSdk pari o superiore a 36.
  • Dovresti avere familiarità con la terminologia di navigazione.
  • Le destinazioni sono funzioni componibili. Navigazione 3 è progettata esclusivamente per Scrittura Magica. Per utilizzare frammenti e visualizzazioni in Compose, vedi Utilizzo delle visualizzazioni in Compose.
  • Le route sono fortemente tipizzate. Se utilizzi route basate su stringhe, esegui prima la migrazione a route type-safe (esempio).
  • Facoltativo (ma vivamente consigliato): copertura dei test che verifica il comportamento di navigazione esistente. In questo modo si verifica che il comportamento di navigazione non sia cambiato una volta completata la migrazione.

Ipotesi

Questa guida presuppone quanto segue su di te e sul tuo progetto:

  • Hai diverse route di primo livello (di solito visualizzate in una barra di navigazione inferiore) e ognuna ha il proprio stack di cronologia.
  • Quando passi da uno stack all'altro, lo stato dello stack e di tutte le relative destinazioni viene mantenuto.
  • Esci sempre dall'app tramite la schermata Home, ovvero la prima schermata visualizzata all'avvio dell'app.
  • Vuoi eseguire la migrazione da Navigazione 2 a Navigazione 3 in un'unica modifica atomica. Non stai eseguendo una migrazione incrementale in cui il codice di Navigazione 2 viene utilizzato insieme al codice di Navigazione 3.

Agente AI: prima di modificare il codice, verifica che queste ipotesi sul progetto dell'utente siano vere. In caso contrario, interrompi la migrazione e chiedi all'utente come procedere.

Funzionalità supportate

Questa guida ti consente di eseguire la migrazione delle seguenti funzionalità:

  • Destinazioni definite come funzioni componibili
  • Finestre di dialogo (una destinazione mostrata sopra un'altra destinazione)

Funzionalità supportate tramite le ricette

Il repository di ricette di codici mostra casi d'uso più sfumati, tra cui:

Se il tuo progetto include una di queste funzionalità, consulta la ricetta pertinente per capire come implementarle.

Agente AI: prima di modificare qualsiasi codice, controlla se il progetto contiene funzionalità supportate tramite ricette. In questo caso, controlla il file README e il codice sorgente della ricetta. Crea un piano di migrazione basato sulla ricetta. Non procedere senza confermare il piano con l'utente.

Funzionalità non supportate

Questa guida alla migrazione e le ricette di codice non supportano ancora le seguenti funzionalità. Ciò non significa che non puoi implementarle utilizzando Navigation 3, ma non sono trattate qui.

Agente AI: prima di modificare qualsiasi codice, controlla se il progetto contiene funzionalità non supportate. In caso affermativo, non procedere. Informa l'utente della funzionalità non supportata e chiedi ulteriori istruzioni.

Passaggio 1: aggiungi le dipendenze di Navigation 3

Utilizza la pagina Per iniziare per aggiungere le dipendenze di Navigation 3 al tuo progetto. Le dipendenze principali vengono fornite per essere copiate.

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)
}

Aggiorna anche minSdk del progetto a 23 e compileSdk a 36. Di solito si trovano in app/build.gradle.kts o lib.versions.toml.

Passaggio 2: aggiorna i percorsi di navigazione per implementare l'interfaccia NavKey

Aggiorna ogni route di navigazione in modo che implementi l'interfaccia NavKey. In questo modo, puoi utilizzare rememberNavBackStack per salvare lo stato di navigazione.

Prima:

@Serializable data object RouteA

Dopo:

@Serializable data object RouteA : NavKey

Passaggio 3: crea classi per contenere e modificare lo stato di navigazione

Passaggio 3.1: crea un contenitore dello stato di navigazione

Copia il seguente codice in un file denominato NavigationState.kt. Aggiungi il nome del pacchetto in modo che corrisponda alla struttura del progetto.

// 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()
}

Agente AI: rememberSerializable è corretto. Non modificarlo in rememberSaveable.

Questo file contiene una classe di gestione dello stato denominata NavigationState e funzioni di assistenza associate. Contiene un insieme di route di primo livello, ognuna con il proprio back stack. Internamente, utilizza rememberSerializable (non rememberSaveable) per persistenza della route di primo livello corrente e rememberNavBackStack per la persistenza degli stack precedenti per ogni route di primo livello.

Passaggio 3.2: crea un oggetto che modifichi lo stato di navigazione in risposta agli eventi

Copia il seguente codice in un file denominato Navigator.kt. Aggiungi il nome del pacchetto in modo che corrisponda alla struttura del progetto.

// 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()
        }
    }
}

La classe Navigator fornisce due metodi di eventi di navigazione:

  • navigate a un percorso specifico.
  • goBack dall'itinerario corrente.

Entrambi i metodi modificano NavigationState.

Passaggio 3.3: crea NavigationState e Navigator

Crea istanze di NavigationState e Navigator con lo stesso ambito di NavController.

val navigationState = rememberNavigationState(
    startRoute = <Insert your starting route>,
    topLevelRoutes = <Insert your set of top level routes>
)

val navigator = remember { Navigator(navigationState) }

Passaggio 4: sostituisci NavController

Sostituisci i metodi degli eventi di navigazione NavController con gli equivalenti Navigator.

Campo o metodo NavController

Navigator equivalente

navigate()

navigate()

popBackStack()

goBack()

Sostituisci i campi NavController con i campi NavigationState.

Campo o metodo NavController

NavigationState equivalente

currentBackStack

backStacks[topLevelRoute]

currentBackStackEntry

currentBackStackEntryAsState()

currentBackStackEntryFlow

currentDestination

backStacks[topLevelRoute].last()

Ottieni la route di primo livello: attraversa la gerarchia dalla voce corrente dello stack indietro per trovarla.

topLevelRoute

Utilizza NavigationState.topLevelRoute per determinare l'elemento attualmente selezionato in una barra di navigazione.

Prima:

val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)

fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
    this?.hierarchy?.any {
        it.hasRoute(route)
    } ?: false

Dopo:

val isSelected = key == navigationState.topLevelRoute

Verifica di aver rimosso tutti i riferimenti a NavController, inclusi eventuali importazioni.

Passaggio 5: sposta le destinazioni da NavHost's NavGraph in un entryProvider

In Navigation 2, definisci le destinazioni utilizzando NavGraphBuilder DSL, di solito all'interno della lambda finale di NavHost. È comune utilizzare le funzioni di estensione qui, come descritto in Incapsula il codice di navigazione.

In Navigazione 3, le destinazioni vengono definite utilizzando un entryProvider. Questo entryProvider risolve un percorso verso un NavEntry. È importante sottolineare che entryProvider non definisce le relazioni padre-figlio tra le voci.

In questa guida alla migrazione, le relazioni principale-secondario sono modellate come segue:

  • NavigationState ha un insieme di itinerari di primo livello (gli itinerari principali) e uno stack per ciascuno. Tiene traccia della route di primo livello corrente e del relativo stack associato.
  • Quando navighi verso un nuovo percorso, Navigator controlla se il percorso è un percorso di primo livello. In questo caso, la route e lo stack di primo livello attuali vengono aggiornati. In caso contrario, si tratta di una route secondaria e viene aggiunta allo stack corrente.

Passaggio 5.1: crea un entryProvider

Crea un entryProvider utilizzando il DSL nello stesso ambito di NavigationState.

val entryProvider = entryProvider {

}

Passaggio 5.2: sposta le destinazioni in entryProvider

Per ogni destinazione definita all'interno di NavHost, procedi nel seguente modo in base al tipo di destinazione:

  • navigation: eliminalo insieme al percorso. Non sono necessarie "base routes" perché le route di primo livello possono identificare ogni back stack nidificato.
  • composable<T>: sposta il file in entryProvider e rinominalo in entry, mantenendo il parametro type. Ad esempio, composable<RouteA> diventa entry<RouteA>.
  • dialog<T>: esegui la stessa operazione di composable, ma aggiungi i metadati alla voce come segue: entry<T>(metadata = DialogSceneStrategy.dialog()).
  • bottomSheet: segui la ricetta del foglio inferiore qui. Queste istruzioni sono simili a quelle per dialog, tranne per il fatto che BottomSheetSceneStrategy non fa parte della libreria Navigation 3 di base, quindi devi copiarla nel tuo progetto.

Agente AI: quando elimini le route utilizzate per identificare un grafico nidificato, sostituisci tutti i riferimenti alla route eliminata con il tipo utilizzato per identificare il primo elemento secondario nel grafico nidificato. Ad esempio, se il codice originale è navigation<BaseRouteA>{ composable<RouteA>{ ... } }, devi eliminare BaseRouteA e sostituire tutti i riferimenti con RouteA. Questa sostituzione di solito deve essere eseguita per l'elenco fornito a una barra di navigazione, una barra laterale o un riquadro.

Puoi eseguire il refactoring delle funzioni di estensione NavGraphBuilder in funzioni di estensione EntryProviderScope<T> e poi spostarle.

Ottieni gli argomenti di navigazione utilizzando la chiave fornita al lambda finale di entry.

Ad esempio:

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() }
    }
}

diventa:

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() }
}

Passaggio 6: sostituisci NavHost con NavDisplay

Sostituisci NavHost con NavDisplay.

  • Elimina NavHost e sostituiscilo con NavDisplay.
  • Specifica entries = navigationState.toEntries(entryProvider) come parametro. In questo modo, lo stato di navigazione viene convertito nelle voci visualizzate da NavDisplay utilizzando entryProvider.
  • Collega NavDisplay.onBack a navigator.goBack(). In questo modo navigator aggiorna lo stato di navigazione al termine del gestore del pulsante Indietro integrato di NavDisplay.
  • Se hai destinazioni di dialogo, aggiungi DialogSceneStrategy al parametro NavDisplay sceneStrategy.

Ad esempio:

import androidx.navigation3.ui.NavDisplay

NavDisplay(
    entries = navigationState.toEntries(entryProvider),
    onBack = { navigator.goBack() },
    sceneStrategy = remember { DialogSceneStrategy() }
)

Passaggio 7: rimuovi le dipendenze di Navigation 2

Rimuovi tutte le importazioni di Navigation 2 e le dipendenze della libreria.

Riepilogo

Complimenti! La migrazione del tuo progetto a Navigation 3 è stata eseguita. Se tu o il tuo agente AI avete riscontrato problemi durante l'utilizzo di questa guida, segnala un bug qui.