Competenze Android
Visualizza su GitHubJetpack Navigation 3
android skills add navigation-3Per eseguire la migrazione dell'app da Navigation 2 a Navigation 3, segui questi passaggi:
- Aggiungi le dipendenze di Navigation 3.
- Aggiorna le tue 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
NavGraphdiNavHostin unentryProvider. - Sostituisci
NavHostconNavDisplay. - Rimuovi le dipendenze di Navigazione 2.
Preparazione
Le sezioni seguenti descrivono i prerequisiti per la migrazione e le ipotesi sul tuo progetto. Inoltre, coprono le funzionalità supportate per la migrazione e quelle non supportate.
Prerequisiti
- Devi utilizzare un
compileSdkpari o superiore a 36. - Dovresti avere familiarità con la terminologia di navigazione.
- Le destinazioni sono funzioni componibili. Navigazione 3 è progettato esclusivamente per Scrittura Magica. Per utilizzare frammenti e visualizzazioni in Compose, vedi Utilizzo di 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 una o più route di primo livello (di solito visualizzate in una barra di navigazione inferiore) e ognuna ha il proprio back stack.
- 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, che è 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)
Funzionalità supportate tramite le ricette
Il repository di ricette di codice mostra casi d'uso più sfumati, tra cui:
- Fogli inferiori (le istruzioni sono fornite in questa guida)
- Codice di navigazione modulare 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 implementarle.
Agente AI: prima di modificare qualsiasi codice, verifica 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.
- Più di un livello di navigazione nidificata
- Destinazioni condivise: schermate che possono spostarsi tra diversi back stack
- Tipi di destinazione personalizzati
Agente AI: prima di modificare il codice, verifica se il progetto contiene una delle funzionalità non supportate. In questo caso, 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 tre dipendenze di Navigation al tuo progetto. Le dipendenze principali vengono fornite per la copia.
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)
}
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 contenitore di stato denominata NavigationState e funzioni helper associate. Contiene un insieme di route di primo livello, ognuna con il proprio stack
di back-end. Internamente, utilizza rememberSerializable (non rememberSaveable) per
persistente la route di primo livello corrente e rememberNavBackStack per rendere persistenti
gli 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:
navigatea un percorso specifico.goBackdall'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 |
|
|---|---|
|
|
|
|
|
|
Sostituisci i campi NavController con i campi NavigationState.
Campo o metodo |
|
|---|---|
|
|
|
|
|
|
|
|
Ottieni la route di primo livello: attraversa la gerarchia dalla voce corrente dello stack indietro 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
eventuali importazioni.
Passaggio 4.1: esegui la migrazione della logica basata sul 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 Navigazione 3, NavDisplay fornisce un LifecycleOwner
ambito di voce tramite LocalLifecycleOwner.current a ogni contenuto
componibile della destinazione. Per saperne di più, consulta la sezione Ciclo di vita della destinazione.
Devi eseguire operazioni sensibili al 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 dello stack indietro:
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 4.2: esegui la migrazione del trasferimento dei risultati
In Navigazione 2, le destinazioni hanno restituito i risultati alle destinazioni precedenti utilizzando
il SavedStateHandle su NavBackStackEntry. Poiché SavedStateHandle è
supportato dallo stato dell'istanza salvato, i dati restituiti sono sopravvissuti automaticamente all'interruzione del processo.
Prima:
// 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) }
In Navigazione 3, aggiungi
rememberResultEventBusNavEntryDecorator() al tuo
NavDisplay.entryDecorators. Nella mappatura entryProvider
della destinazione del mittente, recupera LocalResultEventBus.current
e chiama sendResult(). Nella destinazione di ricezione, utilizza
ResultEffect per inoltrare l'evento a un ViewModel o
attivare un effetto collaterale.
Dopo:
// 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) }
Per l'osservazione basata sullo stato, puoi chiamare il numero resultBus.conflateAsState(). Per
maggiori dettagli, vedi Risultati restituiti.
Passaggio 5: sposta le destinazioni da NavHost's NavGraph in un entryProvider
In Navigation 2, definisci le destinazioni
utilizzando il DSL NavGraphBuilder,
di solito all'interno della lambda finale di NavHost. È prassi 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 nel seguente modo:
NavigationStateha 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,
Navigatorcontrolla 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 con lo stesso ambito di
NavigationState.
val entryProvider = entryProvider<NavKey> { }
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 all'itinerario. Non sono necessarie "base routes" perché le route di primo livello possono identificare ogni back stack nidificato.composable<T>: sposta il file inentryProvidere rinominalo inentry, mantenendo il parametro type. 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. Queste istruzioni sono simili a quelle perdialog, tranne per il fatto cheBottomSheetSceneStrategynon fa parte della libreria principale Navigation 3, quindi devi copiarlo 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 a scomparsa.
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 @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() } } }
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 visualizzate daNavDisplayutilizzandoentryProvider. - Collega
NavDisplay.onBackanavigator.goBack(). In questo modonavigatoraggiorna lo stato di navigazione al termine della gestione del pulsante Indietro integrato diNavDisplay. - Se hai destinazioni di dialogo, aggiungi
DialogSceneStrategyal parametroNavDisplaysceneStrategies.
Ad esempio:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Passaggio 7: esegui la migrazione dei deep link
In Navigazione 2, i link diretti sono stati definiti direttamente all'interno del grafico di navigazione
utilizzando il parametro deepLinks delle destinazioni.
Nella navigazione 3, i link diretti vengono gestiti indipendentemente dall'interfaccia utente di navigazione. Definisci DeepLinkMatcher e abbina le richieste in entrata nella tua Activity per costruire il back stack iniziale.
Prima:
In Navigation 2, potresti aver definito un link diretto come questo:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Dopo:
In Navigazione 3, definisci un UriDeepLinkMatcher per l'itinerario:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Poi, nell'onCreate (e onNewIntent) dell'Activity, abbina l'intent in entrata e inizializza il back stack:
val deepLinkMatchers: List<DeepLinkMatcher<*, *>> = listOf( userMatcher, ) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val request = DeepLinkRequest(intent = intent) val matchResult = deepLinkMatchers .mapNotNull { it.match(request) } .maxOrNull() val backStack = when (matchResult) { null -> listOf(HomeKey) is BackStackMatchResult<*, *> -> { @Suppress("UNCHECKED_CAST") matchResult.backStack as List<NavKey> } else -> listOf(matchResult.key) } // Use backStack with NavDisplay } }
Tipi di argomenti personalizzati
In Navigazione 2, hai gestito i tipi di argomenti personalizzati o di terze parti (ad esempio
LocalDateTime) utilizzando implementazioni NavType personalizzate e typeMap.
In Navigation 3, definisci un DeepLinkSerializer per deserializzare i tipi personalizzati o
di terze parti dai parametri URI. Per maggiori dettagli, consulta
Serializzazione personalizzata con DeepLinkSerializer.
Per casi d'uso più avanzati, inclusi stack di chiamate sintetici e matcher personalizzati, consulta la guida Supportare i deep link.
Passaggio 8: rimuovi le dipendenze di Navigation 2
Rimuovi tutte le importazioni di Navigation 2 e le dipendenze della libreria.
Riepilogo
Complimenti! La migrazione del progetto a Navigazione 3 è stata completata. Se tu o il tuo agente AI avete riscontrato problemi durante l'utilizzo di questa guida, segnala un bug qui.