Habilidades de Android
Ver en GitHubJetpack Navigation 3
android skills add navigation-3Para 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 contener y modificar tu estado de navegación.
- Reemplaza
NavControllerpor estas clases. - Mueve tus destinos de
NavHost'sNavGrapha unentryProvider. - Reemplaza
NavHostporNavDisplay. - Quita las dependencias de Navigation 2.
Preparación
En las siguientes secciones, se describen los requisitos previos para la migración y las suposiciones sobre tu proyecto. También se abarcan las funciones compatibles con la migración y las que no lo son.
Requisitos previos
- Debes usar un
compileSdkde 36 o versiones posteriores. - Debes estar familiarizado con la terminología de navegación.
- Los destinos son funciones que admiten composición. Navigation 3 está diseñado exclusivamente para Compose. Para usar fragmentos y vistas en Compose, consulta Cómo usar objetos View en Compose.
- Las rutas tienen tipos definidos. Si usas rutas basadas en cadenas, migra a rutas con seguridad de tipos primero (ejemplo).
- Opcional (pero muy recomendado): Cobertura de pruebas que verifica 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 una o varias rutas de nivel superior (que suelen mostrarse en una barra de navegación inferior), y cada una tiene su propia pila de actividades.
- 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 pantalla 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. Si no es así, detén la migración y pregúntale al usuario cómo proceder.
Funciones admitidas
Esta guía te permite migrar las siguientes funciones:
- Destinos definidos como funciones que admiten composición
- Diálogos (un destino que se muestra sobre otro destino)
Funciones compatibles a través de recetas
El repositorio de recetas de código demuestra 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
- Uso y paso de argumentos a
ViewModel - Devolución de resultados desde una pantalla
Si tu proyecto tiene alguna de estas funciones, consulta la receta pertinente para comprender cómo implementarlas.
Agente de IA: Antes de cambiar cualquier código, verifica si el proyecto contiene alguna función compatible a través de 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 compatibles
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 implementarlas con Navigation 3; simplemente no se abarcan aquí.
- Más de un nivel de navegación anidada
- Destinos compartidos: pantallas que se pueden mover entre diferentes pilas de actividades
- Tipos de destino personalizados
Agente de IA: Antes de cambiar cualquier código, verifica si el proyecto contiene alguna de las funciones no compatibles. Si es así, no continúes. Informa al usuario sobre la función no compatible y solicita más instrucciones.
Paso 1: Agrega dependencias de Navigation 3
Usa la página Primeros pasos 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.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)
}
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 navegación ruta para que implemente la NavKey
interfaz. Esto te permite usar rememberNavBackStack para ayudar a guardar tu
estado de navegación.
Antes:
@Serializable data object RouteA
Después:
@Serializable data object RouteA : NavKey
Paso 3: Crea clases para contener y modificar tu estado de navegación
Paso 3.1: Crea un contenedor de estado de navegación
Copia el siguiente código en un archivo llamado NavigationState.kt. Agrega el nombre del 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 contenedor de estado llamada NavigationState y funciones auxiliares 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 actual de nivel superior y rememberNavBackStack para conservar las pilas de actividades 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 del 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íficagoBackdesde la ruta actual
Ambos métodos modifican el NavigationState.
Paso 3.3: Crea el NavigationState y el 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 equivalentes de Navigator.
Campo o método |
Equivalente de |
|---|---|
|
|
|
|
Reemplaza los campos NavController por campos NavigationState.
Campo o método |
Equivalente de |
|---|---|
|
|
|
|
Obtén la ruta de nivel superior: Recorre la jerarquía desde la entrada actual de la pila de actividades para encontrarla. |
|
Usa NavigationState.topLevelRoute para determinar el elemento 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 4.1: Migra la lógica optimizada para el ciclo de vida
En Navigation 2, NavBackStackEntry implementa LifecycleOwner, lo que te permite escuchar eventos del ciclo de vida o recopilar flujos de una manera optimizada para el ciclo de vida con navController.currentBackStackEntry.
En Navigation 3, NavDisplay proporciona un LifecycleOwner
con alcance de entrada a través de LocalLifecycleOwner.current al contenido componible de cada destino. Consulta Ciclo de vida de destino para obtener más información.
Debes realizar operaciones optimizadas para el ciclo de vida directamente dentro del contenido componible de tu destino haciendo referencia a LocalLifecycleOwner.current.
Por ejemplo, si recopilas un flujo de una manera optimizada para el ciclo de vida con la entrada de la pila de actividades:
Antes:
// In your destination screen or host val lifecycleOwner = navController.currentBackStackEntry!! val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner)
Después:
// Inside the destination composable val state by flow.collectAsStateWithLifecycle()
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 a un NavEntry. Es importante destacar que el entryProvider no define relaciones de superior y secundario entre las entradas.
En esta guía de migración, las relaciones de superior y secundario se modelan de la siguiente manera:
NavigationStatetiene un conjunto de rutas de nivel superior (las rutas superiores) y una pila para cada una. Realiza un seguimiento de la ruta actual de nivel superior y su pila asociada.- Cuando se navega a una ruta nueva,
Navigatorverifica si la ruta es de nivel superior. Si es así, se actualizan la ruta y la pila actuales de nivel superior. Si no es así, 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<NavKey> { }
Paso 5.2: Mueve los destinos al 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 es necesario tener "rutas base" porque las rutas de nivel superior pueden identificar cada pila de actividades anidada.composable<T>: Muévelo aentryProvidery cambia su nombre aentry, conservando el parámetro de tipo. Por ejemplo,composable<RouteA>se convierte enentry<RouteA>.dialog<T>: Haz lo mismo que concomposable, pero agrega metadatos a la entrada de la siguiente manera:entry<T>(metadata = DialogSceneStrategy.dialog()).bottomSheet: Sigue la receta de la hoja inferior aquí. 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 borres las rutas que se usan para identificar un gráfico anidado, reemplaza cualquier
referencia 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, un carril o un panel lateral de navegación.
Puedes refactorizar NavGraphBuilder funciones de extensión a
EntryProviderScope<T> funciones de extensión 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 @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() } } }
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 por NavDisplay.
- Borra
NavHosty reemplázalo porNavDisplay. - Especifica
entries = navigationState.toEntries(entryProvider)como parámetro. Esto convierte el estado de navegación en las entradas queNavDisplaymuestra con elentryProvider. - Conecta
NavDisplay.onBackanavigator.goBack(). Esto hace quenavigatoractualice el estado de navegación cuando se completa el controlador de retroceso integrado deNavDisplay. - Si tienes destinos de diálogo, agrega
DialogSceneStrategyal parámetrosceneStrategiesdeNavDisplay.
Por ejemplo:
NavDisplay( entries = navigationState.toEntries(entryProvider), onBack = { navigator.goBack() }, sceneStrategies = remember { listOf(DialogSceneStrategy()) } )
Paso 7: Migra vínculos directos
En Navigation 2, los vínculos directos se definían directamente dentro del gráfico de navegación con el parámetro deepLinks de los destinos.
En Navigation 3, los vínculos directos se administran de forma independiente de la IU de navegación. Defines DeepLinkMatchers y haces coincidir las solicitudes entrantes en tu actividad para construir la pila de actividades inicial.
Antes:
En Navigation 2, es posible que hayas definido un vínculo directo como este:
composable<RouteA>( deepLinks = listOf( navDeepLink { uriPattern = "www.example.com/user/{id}" } ) ) { // ... }
Después:
En Navigation 3, defines un UriDeepLinkMatcher para la ruta:
val userMatcher = UriDeepLinkMatcher( DeepLinkUri("www.example.com/user/{id}"), serializer<RouteA>() )
Luego, en el onCreate (y onNewIntent) de tu actividad, haces coincidir el intent entrante y, luego, inicializas tu pila de actividades:
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 } }
Tipos de argumentos personalizados
En Navigation 2, manejabas tipos de argumentos personalizados o de terceros (como LocalDateTime) con implementaciones NavType personalizadas y typeMap.
En Navigation 3, define un DeepLinkSerializer para deserializar tipos personalizados o
de terceros de parámetros URI. Consulta
Serialización personalizada con DeepLinkSerializer para obtener más detalles.
Para ver casos de uso más avanzados, incluidas las pilas de actividades sintéticas y los comparadores personalizados, consulta la guía Admite vínculos directos.
Paso 8: Quita las dependencias de Navigation 2
Quita todas las importaciones de Navigation 2 y las dependencias de biblioteca.
Resumen
¡Felicitaciones! Tu proyecto ahora se migró a Navigation 3. Si tú o tu agente de IA tuvieron algún problema con esta guía, informa un error aquí.