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 mantener y modificar tu estado de navegación.
- Reemplaza
NavControllerpor estas clases. - Mueve tus destinos del
NavGraphdeNavHosta unentryProvider. - Reemplaza
NavHostporNavDisplay. - Se quitaron 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 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 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 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 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 detallados, 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
ViewModel - 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 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 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
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.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 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 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 correcta. 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 historial. Internamente, usa rememberSerializable (no rememberSaveable) para conservar la ruta de nivel superior actual y rememberNavBackStack para conservar los historiales de navegación 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 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 4.1: Migra la lógica optimizada para el ciclo de vida
En Navigation 2, NavBackStackEntry implementa LifecycleOwner, lo que te permite escuchar eventos de ciclo de vida o recopilar flujos de forma 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 que tengan en cuenta el 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 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 4.2: Migra el paso de resultados
En Navigation 2, los destinos devolvían resultados a destinos anteriores con SavedStateHandle en NavBackStackEntry. Como SavedStateHandle se basa en 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 de entryProvider del destino del 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: 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 de superior y secundario 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<NavKey> { }
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 las funciones de extensión NavGraphBuilder en funciones de extensión EntryProviderScope<T> y, luego, moverlas.
Obtén argumentos de navegación con la clave proporcionada al 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 un 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 atrás 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. Defines DeepLinkMatchers y haces coincidir las solicitudes entrantes en tu actividad para construir la pila de actividades inicial.
Antes:
En Navigation 2, podrías haber definido un vínculo directo de la siguiente manera:
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 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, controlaste los tipos de argumentos personalizados o de terceros (como LocalDateTime) con implementaciones de NavType personalizadas 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, como pilas atrás 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 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í.