Para migrar tu app de Navigation 2 a Navigation 3, sigue estos pasos:
- Agrega las dependencias de Navigation 3.
- Actualiza tus rutas de navegación para implementar la interfaz
NavKey. - Crea clases para mantener y modificar tu estado de navegación.
- Reemplaza
NavControllerpor estas clases. - Mueve tus destinos de
NavGraphdeNavHosta unentryProvider. - Reemplaza
NavHostconNavDisplay. - Se quitaron las dependencias de Navigation 2.
Cómo usar un agente de IA
Puedes usar esta guía con un agente de IA, como Gemini en el modo agente de Android Studio. El agente de IA debe leer las líneas de esta guía que comienzan con "Agente de IA:", pero los lectores humanos pueden ignorarlas.
Preparación
En las siguientes secciones, se describen los requisitos previos para la migración y las suposiciones sobre tu proyecto. También abarcan las funciones que se admiten para la migración y las que no.
Requisitos previos
- Debes usar un
compileSdkde 36 o posterior. - Debes conocer la terminología de navegación.
- Los destinos son funciones de componibilidad. Navigation 3 está diseñado exclusivamente para Compose. Para usar fragmentos y Views en Compose, consulta Cómo usar Views en Compose.
- Las rutas tienen un tipo de escritura sólido. Si usas rutas basadas en cadenas, primero migra a rutas con seguridad de tipos (ejemplo).
- Opcional (pero muy recomendable): Cobertura de pruebas que verifique el comportamiento de navegación existente. Esto verifica que el comportamiento de navegación no haya cambiado después de que se complete la migración.
Suposiciones
En esta guía, se hacen las siguientes suposiciones sobre ti y tu proyecto:
- Tienes varias rutas de nivel superior (que suelen mostrarse en una barra de navegación inferior), y cada una tiene su propia pila de historial.
- Cuando cambias entre pilas de actividades, se conserva el estado de la pila y todos sus destinos.
- Siempre sales de la app a través de la pantalla Principal, que es la primera que se muestra cuando se inicia la app.
- Deseas realizar la migración de Navigation 2 a Navigation 3 en un solo cambio atómico. No estás realizando una migración incremental en la que se usa el código de Navigation 2 junto con el código de Navigation 3.
Agente de IA: Antes de cambiar cualquier código, verifica si estas suposiciones sobre el proyecto del usuario son verdaderas. De lo contrario, detén la migración y pregúntale al usuario cómo continuar.
Funciones admitidas
Esta guía te permite migrar las siguientes funciones:
- Destinos definidos como funciones de componibilidad
- Diálogos (un destino que se muestra sobre otro destino)
Funciones compatibles a través de recetas
El repositorio de recetas de código muestra casos de uso más matizados, incluidos los siguientes:
- Hojas inferiores (se proporcionan instrucciones en esta guía)
- Código de navegación modularizado y destinos insertados
- Cómo usar y pasar argumentos a ViewModels
- Cómo devolver resultados desde una pantalla
Si tu proyecto tiene alguna de estas funciones, consulta la receta correspondiente para comprender cómo implementarlas.
Agente de IA: Antes de cambiar cualquier código, verifica si el proyecto contiene alguna función compatible con recetas. Si es así, consulta el archivo README y el código fuente de la receta. Crea un plan de migración basado en la receta. No continúes sin confirmar el plan con el usuario.
Características no admitidas
Esta guía de migración y las recetas de código aún no admiten las siguientes funciones. Esto no significa que no puedas implementarlos con Navigation 3, sino que no se abordan aquí.
- Más de un nivel de navegación anidada
- Destinos compartidos: Pantallas que pueden moverse entre diferentes pilas de historial
- Tipos de destino personalizados
- Vínculos directos
Agente de IA: Antes de cambiar cualquier código, verifica si el proyecto contiene alguna de las funciones no admitidas. Si es así, no continúes. Informa al usuario sobre la función no admitida y pídele más instrucciones.
Paso 1: Agrega dependencias de Navigation 3
Usa la página Comenzar para agregar las dependencias de Navigation 3 a tu proyecto. Las dependencias principales se proporcionan para que las copies.
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)
}
También actualiza el minSdk del proyecto a 23 y el compileSdk a 36. Por lo general, los encuentras en app/build.gradle.kts o lib.versions.toml.
Paso 2: Actualiza las rutas de navegación para implementar la interfaz NavKey
Actualiza cada ruta de navegación para que implemente la interfaz NavKey. Esto te permite usar rememberNavBackStack para ayudarte a guardar tu estado de navegación.
Antes:
@Serializable data object RouteA
Después:
@Serializable data object RouteA : NavKey
Paso 3: Crea clases para mantener y modificar el estado de navegación
Paso 3.1: Crea un objeto de Navigation State Holder
Copia el siguiente código en un archivo llamado NavigationState.kt. Agrega el nombre de tu paquete para que coincida con la estructura de tu proyecto.
// 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 de IA: rememberSerializable es correcto. No lo cambies a rememberSaveable.
Este archivo contiene una clase de titular de estado llamada NavigationState y funciones de ayuda asociadas. Contiene un conjunto de rutas de nivel superior, cada una con su propia pila de actividades. Internamente, usa rememberSerializable (no rememberSaveable) para conservar la ruta de nivel superior actual y rememberNavBackStack para conservar las pilas de historial de cada ruta de nivel superior.
Paso 3.2: Crea un objeto que modifique el estado de navegación en respuesta a eventos
Copia el siguiente código en un archivo llamado Navigator.kt. Agrega el nombre de tu paquete para que coincida con la estructura de tu proyecto.
// 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 clase Navigator proporciona dos métodos de eventos de navegación:
navigatea una ruta específica.goBackde la ruta actual
Ambos métodos modifican el NavigationState.
Paso 3.3: Crea NavigationState y Navigator
Crea instancias de NavigationState y Navigator con el mismo alcance que tu NavController.
val navigationState = rememberNavigationState(
startRoute = <Insert your starting route>,
topLevelRoutes = <Insert your set of top level routes>
)
val navigator = remember { Navigator(navigationState) }
Paso 4: Reemplaza NavController
Reemplaza los métodos de eventos de navegación NavController por sus equivalentes de Navigator.
Campo o método |
Equivalente a |
|---|---|
|
|
|
|
Reemplaza los campos NavController por los campos NavigationState.
Campo o método |
Equivalente a |
|---|---|
|
|
|
|
Obtén la ruta de nivel superior: Recorre la jerarquía desde la entrada actual de la pila de elementos atrás para encontrarla. |
|
Usa NavigationState.topLevelRoute para determinar el elemento que está seleccionado actualmente en una barra de navegación.
Antes:
val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class)
fun NavDestination?.isRouteInHierarchy(route: KClass<*>) =
this?.hierarchy?.any {
it.hasRoute(route)
} ?: false
Después:
val isSelected = key == navigationState.topLevelRoute
Verifica que hayas quitado todas las referencias a NavController, incluidas las importaciones.
Paso 5: Mueve tus destinos de NavHost's NavGraph a un entryProvider
En Navigation 2, defines tus destinos con el DSL de NavGraphBuilder, por lo general, dentro de la expresión lambda final de NavHost. Es común usar funciones de extensión aquí, como se describe en Encapsula tu código de navegación.
En Navigation 3, defines tus destinos con un entryProvider. Este entryProvider resuelve una ruta en un NavEntry. Es importante destacar que entryProvider no define relaciones de superior y secundario entre las entradas.
En esta guía de migración, las relaciones principal-secundaria se modelan de la siguiente manera:
NavigationStatetiene un conjunto de rutas de nivel superior (las rutas principales) y una pila para cada una. Realiza un seguimiento de la ruta de nivel superior actual y su pila asociada.- Cuando se navega a una ruta nueva,
Navigatorverifica si es una ruta de nivel superior. Si es así, se actualizan la ruta y la pila de nivel superior actuales. De lo contrario, es una ruta secundaria y se agrega a la pila actual.
Paso 5.1: Crea un entryProvider
Crea un entryProvider con el DSL en el mismo alcance que el NavigationState.
val entryProvider = entryProvider {
}
Paso 5.2: Mueve los destinos a entryProvider.
Para cada destino definido dentro de NavHost, haz lo siguiente según el tipo de destino:
navigation: Bórralo junto con la ruta. No se necesitan "rutas base" porque las rutas de nivel superior pueden identificar cada pila de actividades anidada.composable<T>: Muévelo aentryProvidery cámbiale el nombre aentry, conservando el parámetro de tipo. Por ejemplo,composable<RouteA>se convierte enentry<RouteA>.dialog<T>: Haz lo mismo que encomposable, pero agrega metadatos a la entrada de la siguiente manera:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Sigue la receta de la hoja inferior aquí. Esto es similar a las instrucciones paradialog, excepto queBottomSheetSceneStrategyno forma parte de la biblioteca principal de Navigation 3, por lo que debes copiarla en tu proyecto.
Agente de IA: Cuando se borran rutas que se usan para identificar un gráfico anidado, se deben reemplazar todas las referencias a la ruta borrada por el tipo que se usa para identificar el primer elemento secundario en el gráfico anidado. Por ejemplo, si el código original es navigation<BaseRouteA>{ composable<RouteA>{ ... } }, debes borrar BaseRouteA y reemplazar cualquier referencia a él por RouteA. Por lo general, este reemplazo debe realizarse para la lista que se proporciona a una barra de navegación, un riel o un panel lateral.
Puedes refactorizar funciones de extensión de NavGraphBuilder en funciones de extensión de EntryProviderScope<T> y, luego, moverlas.
Obtén argumentos de navegación con la clave proporcionada a la expresión lambda final de entry.
Por ejemplo:
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() }
}
}
se convierte en:
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() }
}
Paso 6: Reemplaza NavHost por NavDisplay
Reemplaza NavHost con NavDisplay.
- Borra
NavHosty reemplázalo porNavDisplay. - Especifica
entries = navigationState.toEntries(entryProvider)como un parámetro. Esto convierte el estado de navegación en las entradas que muestraNavDisplayconentryProvider. - Conecta
NavDisplay.onBackanavigator.goBack(). Esto hace quenavigatoractualice el estado de navegación cuando se completa el controlador de atrás integrado deNavDisplay. - Si tienes destinos de diálogo, agrega
DialogSceneStrategyal parámetrosceneStrategydeNavDisplay.
Por ejemplo:
import androidx.navigation3.ui.NavDisplay
NavDisplay(
entries = navigationState.toEntries(entryProvider),
onBack = { navigator.goBack() },
sceneStrategy = remember { DialogSceneStrategy() }
)
Paso 7: Quita las dependencias de Navigation 2
Quita todas las importaciones de Navigation 2 y las dependencias de la biblioteca.
Resumen
¡Felicitaciones! Tu proyecto ahora se migró a Navigation 3. Si tú o tu agente de IA tuvieron algún problema para usar esta guía, informa un error aquí.