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. - Crear clases para mantener y modificar el estado de navegación
- Reemplaza
NavControllerpor estas clases. - Mueve tus destinos de la
NavGraphdeNavHosta unentryProvider. - Reemplaza
NavHostporNavDisplay. - Quita las dependencias de Navigation 2.
Preparación
En las siguientes secciones, se describen los requisitos 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 se diseñó exclusivamente para Compose. Para usar fragmentos y vistas en Compose, consulta Cómo usar vistas en Compose.
- Las rutas tienen tipos fuertemente definidos. 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 da por sentado lo siguiente sobre ti y tu proyecto:
- Tienes una o varias rutas de nivel superior (que, por lo general, se muestran 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 Home, que es la primera que se muestra cuando se inicia la app.
- Quieres 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 use el código de Navigation 2 junto con el 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 proceder.
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 con las recetas
En el repositorio de recetas de código, se muestran casos de uso más detallados, como los siguientes:
- Hojas inferiores (las instrucciones se proporcionan en esta guía)
- Código de navegación modularizado y destinos insertados
- Cómo usar y pasar argumentos a
ViewModel - Cómo mostrar resultados de 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 con las 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 implementarlos con Navigation 3, sino que no se tratan 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 admitidas. Si es así, no continúes. Informa al usuario sobre la función no compatible y pídele más instrucciones.
Paso 1: Agrega las dependencias de Navigation 3
Usa la página Comenzar para agregar las dependencias de Navigation 3 a tu proyecto. Se proporcionan las dependencias principales para que las copies.
lib.versions.toml
[versions]
nav3Core = "1.2.0"
# 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 encontrarás 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 el 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 contenedor de estado de navegación
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 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. De forma interna, usa rememberSerializable (no rememberSaveable) para conservar la ruta de nivel superior actual 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 los 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 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 de NavController por los equivalentes de Navigator.
Campo o método |
Equivalente a |
|---|---|
|
|
|
|
|
|
Reemplaza los campos NavController por campos NavigationState.
Campo o método |
Equivalente a |
|---|---|
|
|
|
|
|
|
|
|
Obtén la ruta de nivel superior: Recorre la jerarquía hacia arriba desde la entrada actual de la pila de actividades 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 4.1: Migra la lógica optimizada para ciclos de vida
En Navigation 2, NavBackStackEntry implementa LifecycleOwner, lo que te permite escuchar eventos de 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 para el contenido componible
de cada destino. Consulta Ciclo de vida del destino para obtener más información.
Debes realizar operaciones adaptadas al ciclo de vida directamente dentro del contenido componible de tu destino haciendo referencia a LocalLifecycleOwner.current.
Por ejemplo, si recopilas un flujo de manera optimizada para ciclos de vida con la entrada de la pila de actividades, haz lo siguiente:
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 4.2: Migra el paso de resultados
En Navigation 2, los destinos pasaban los resultados a los destinos anteriores con SavedStateHandle en NavBackStackEntry. Debido a que SavedStateHandle está respaldado por el estado de instancia guardado, los datos devueltos sobrevivieron automáticamente al cierre del proceso.
Antes:
// 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) }
En Navigation 3, agrega
rememberResultEventBusNavEntryDecorator() a
tu NavDisplay.entryDecorators. En la asignación entryProvider de tu destino remitente, recupera LocalResultEventBus.current y llama a sendResult(). En el destino receptor, usa ResultEffect para reenviar el evento a un ViewModel o activar un efecto secundario.
Después:
// 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) }
Para la observación basada en el estado, puedes llamar a resultBus.conflateAsState(). Para
obtener más detalles, consulta Devuelve resultados.
Paso 5: Traslada tus destinos de la NavGraph de NavHost 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
entryProvider no define relaciones de superior y secundario entre las entradas.
En esta guía de migración, las relaciones superior-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 de nivel superior actual 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 de nivel superior actuales. Si no es así, se trata de 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: Borrarlo junto con la ruta No es necesario usar "rutas base", ya que las rutas de nivel superior pueden identificar cada pila de actividades anidada.composable<T>: Muévela 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 copiarlo 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 riel o un panel lateral de navegación.
Puedes refactorizar las funciones de extensión de NavGraphBuilder para que sean 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 @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 lo siguiente:
// ... 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 que muestraNavDisplayconentryProvider. - Conectar
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 los 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. Debes definir los DeepLinkMatcher y hacer 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 método onCreate (y onNewIntent) de tu actividad, haz coincidir el intent entrante y, luego, inicializa la 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, controlabas los tipos de argumentos personalizados o de terceros (como LocalDateTime) con implementaciones personalizadas de NavType y typeMap.
En Navigation 3, define un DeepLinkSerializer para deserializar tipos personalizados o de terceros a partir de parámetros de URI. Consulta Serialización personalizada con DeepLinkSerializer para obtener más detalles.
Para ver casos de uso más avanzados, incluidos los de pilas de actividades sintéticas y comparadores personalizados, consulta la guía Cómo admitir vínculos directos.
Paso 8: Quita las dependencias de Navigation 2
Quita todas las importaciones de Navigation 2 y las dependencias de la biblioteca.
Resumen
¡Felicitaciones! Tu proyecto ya se migró a Navigation 3. Si tú o tu agente de IA tuvieron algún problema con esta guía, informa un error aquí.