Per eseguire la migrazione dell'app da Navigation 2 a Navigation 3:
- Aggiungi le dipendenze di Navigation 3.
- Aggiorna le route di navigazione per implementare l'interfaccia
NavKey. - Crea classi per contenere e modificare lo stato di navigazione.
- Sostituisci
NavControllercon queste classi. - Sposta le destinazioni da
NavGraphdiNavHosta unentryProvider. - Sostituisci
NavHostconNavDisplay. - Rimuovi le dipendenze di Navigation 2.
Prompt AI
Eseguire la migrazione da Navigation 2 a Navigation 3
Questo prompt utilizzerà questa guida per eseguire la migrazione a Navigation 3.
Migrate from Navigation 2 to Navigation 3 using the official
migration guide.Se riscontri problemi, segnala un problema qui.
Preparazione
Le sezioni seguenti descrivono i prerequisiti per la migrazione e le ipotesi sul tuo progetto. Vengono inoltre illustrate le funzionalità supportate per la migrazione e quelle non supportate.
Prerequisiti
- Devi utilizzare un
compileSdkdi 36 o versioni successive. - Dovresti conoscere la terminologia di navigazione.
- Le destinazioni sono funzioni componibili. Navigation 3 è progettato esclusivamente per Compose. Per utilizzare Fragment e View in Compose, consulta Utilizzare le View in Compose.
- Le route sono fortemente tipizzate. Se utilizzi route basate su stringhe, esegui prima la migrazione alle route con sicurezza dei tipi (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 dopo il completamento della migrazione.
Ipotesi
Questa guida presuppone quanto segue su di te e sul tuo progetto:
- Hai una o più route di primo livello (in genere visualizzate in una barra di navigazione in basso) e ognuna ha il proprio back stack.
- Quando passi da un back 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 Navigation 2 a Navigation 3 in un'unica modifica atomica. Non stai eseguendo una migrazione incrementale in cui il codice di Navigation 2 viene utilizzato insieme al codice di Navigation 3.
Agente AI: prima di modificare il codice, verifica se queste ipotesi sul progetto dell'utente sono 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 delle ricette di codice mostra casi d'uso più sfumati, tra cui:
- Fogli inferiori (le istruzioni sono fornite in questa guida)
- Codice di navigazione modularizzato e destinazioni inserite
- Utilizzo e passaggio di argomenti a
ViewModel - Restituzione dei risultati da una schermata
Se il tuo progetto include una di queste funzionalità, consulta la ricetta pertinente per capire come implementarla.
Agente AI: prima di modificare il codice, verifica se il progetto contiene funzionalità supportate tramite le ricette. In caso affermativo, controlla il file README e il codice sorgente della ricetta. Crea un piano di migrazione basato sulla ricetta. Non procedere senza aver confermato 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.
- Più di un livello di navigazione nidificata
- Destinazioni condivise: schermate che possono spostarsi tra diversi back stack
- Tipi di destinazione personalizzati
- Link diretti
Agente AI: prima di modificare il codice, verifica se il progetto contiene una delle 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 Guida introduttiva per aggiungere le dipendenze di Navigation 3 al tuo progetto. Le dipendenze principali sono fornite per la copia.
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. In genere, questi valori si trovano in app/build.gradle.kts o lib.versions.toml.
Passaggio 2: aggiorna le route 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 contenitore di stato denominata NavigationState e le funzioni di assistenza associate. Contiene un insieme di route di primo livello, ognuna con il proprio back stack. Internamente, utilizza rememberSerializable (non rememberSaveable) per rendere persistente la route di primo livello corrente e rememberNavBackStack per rendere persistenti i back stack per ogni route di primo livello.
Passaggio 3.2: crea un oggetto che modifica 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:
navigatea una route specifica.goBackdalla route 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 di eventi di navigazione NavController con gli equivalenti di Navigator.
Campo o metodo |
Equivalente |
|---|---|
|
|
|
|
Sostituisci i campi NavController con i campi NavigationState.
Campo o metodo |
Equivalente |
|---|---|
|
|
|
|
Ottieni la route di primo livello: percorri la gerarchia dalla voce del back stack corrente per trovarla. |
|
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 gli import.
Passaggio 4.1: esegui la migrazione della logica con riconoscimento del ciclo di vita
In Navigation 2, NavBackStackEntry implementa LifecycleOwner, consentendoti di ascoltare gli eventi del ciclo di vita o raccogliere i flussi in modo consapevole del ciclo di vita utilizzando navController.currentBackStackEntry.
In Navigation 3, NavDisplay fornisce un LifecycleOwner
con ambito di voce tramite LocalLifecycleOwner.current ai contenuti componibili
di ogni destinazione. Per ulteriori informazioni, consulta Ciclo di vita della destinazione.
Devi eseguire le operazioni con riconoscimento del ciclo di vita direttamente all'interno dei contenuti componibili della destinazione facendo riferimento a LocalLifecycleOwner.current.
Ad esempio, se raccogli un flusso in modo consapevole del ciclo di vita utilizzando la voce del back stack:
Prima:
// In your destination screen or host
val lifecycleOwner = navController.currentBackStackEntry
val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Dopo:
// Inside the destination composable
val state by flow.collectAsStateWithLifecycle()
Passaggio 5: sposta le destinazioni da NavGraph di NavHost a un entryProvider
In Navigation 2, definisci le destinazioni
utilizzando il DSL NavGraphBuilder,
in genere all'interno della lambda finale di NavHost. È comune utilizzare le funzioni di estensione
qui, come descritto in Incapsulare il codice di navigazione.
In Navigation 3, definisci le destinazioni utilizzando un entryProvider. Questo
entryProvider risolve una route in un NavEntry. È importante sottolineare che entryProvider non definisce le relazioni padre-figlio tra le voci.
In questa guida alla migrazione, le relazioni padre-figlio vengono modellate come segue:
NavigationStateha un insieme di route di primo livello (le route principali) e uno stack per ognuna. Tiene traccia della route di primo livello corrente e dello stack associato.- Quando si passa a una nuova route,
Navigatorverifica se la route è di primo livello. In caso affermativo, la route e lo stack di primo livello correnti 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 come segue in base al tipo di destinazione:
navigation: eliminala insieme alla route. Non sono necessarie "route di base" perché le route di primo livello possono identificare ogni back stack nidificato.composable<T>: spostala inentryProvidere rinominala inentry, mantenendo il parametro di tipo. Ad esempio,composable<RouteA>diventaentry<RouteA>.dialog<T>: esegui la stessa operazione dicomposable, ma aggiungi i metadati alla voce come segue:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: segui la ricetta del riquadro inferiore qui. È simile alle istruzioni perdialog, tranne per il fatto cheBottomSheetSceneStrategynon fa parte della libreria principale di Navigation 3, 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. In genere, questa sostituzione deve essere eseguita per l'elenco fornito a una barra di navigazione, una barra laterale o un riquadro a scomparsa.
Puoi eseguire il refactoring delle funzioni di estensione NavGraphBuilder in
EntryProviderScope<T> funzioni di estensione e poi spostarle.
Ottieni gli argomenti di navigazione utilizzando la chiave fornita alla 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
NavHoste sostituiscilo conNavDisplay. - Specifica
entries = navigationState.toEntries(entryProvider)come parametro. In questo modo, lo stato di navigazione viene convertito nelle voci cheNavDisplaymostra utilizzandoentryProvider. - Collega
NavDisplay.onBackanavigator.goBack(). In questo modo,navigatoraggiorna lo stato di navigazione al termine del gestore di back integrato diNavDisplay. - Se hai destinazioni di dialogo, aggiungi
DialogSceneStrategyal parametrosceneStrategiesdiNavDisplay.
Ad esempio:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategies = remember { listOf(DialogSceneStrategy()) }
)
Passaggio 7: rimuovi le dipendenze di Navigation 2
Rimuovi tutti gli import e le dipendenze della libreria di Navigation 2.
Riepilogo
Complimenti! Il progetto è stato migrato a Navigation 3. Se tu o il tuo agente AI avete riscontrato problemi durante l'utilizzo di questa guida, segnala un bug qui.