El componente Navigation proporciona compatibilidad con Jetpack aplicaciones de Compose. Puedes navegar entre funciones de componibilidad a la vez que aprovechas la infraestructura del componente Navigation y atributos.
Configuración
Para admitir Compose, usa la siguiente dependencia en el archivo
Archivo build.gradle
:
Groovy
dependencies { def nav_version = "2.8.0" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.8.0" implementation("androidx.navigation:navigation-compose:$nav_version") }
Comenzar
Cuando implementes la navegación en una app, implementa un host de navegación. grafo y controlador. Para obtener más información, consulta la descripción general de Navigation.
Cómo crear un NavController
Si quieres obtener información para crear un NavController
en Compose, consulta el documento de Compose.
de Cómo crear un controlador de navegación.
Cómo crear un NavHost
Si deseas obtener información para crear un NavHost
en Compose, consulta la sección de Compose.
de Diseña tu gráfico de navegación.
Cómo navegar a un elemento componible
Si deseas obtener información para navegar a un elemento componible, consulta Cómo navegar a un elemento componible destino en la arquitectura en la documentación de Google Cloud.
Cómo navegar con argumentos
Navigation Compose también es compatible con el paso de argumentos entre destinos que admiten composición. Para ello, debes agregar marcadores de posición de argumento a la ruta, de manera similar al proceso de agregar argumentos a un vínculo directo cuando usas la biblioteca de navegación base:
NavHost(startDestination = "profile/{userId}") {
...
composable("profile/{userId}") {...}
}
De forma predeterminada, todos los argumentos se analizan como strings. El parámetro arguments
de
composable()
acepta una lista de objetos NamedNavArgument
. Puedes
crea rápidamente un NamedNavArgument
con el método navArgument()
y
Luego, especifica su type
exacto:
NavHost(startDestination = "profile/{userId}") {
...
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
}
Debes extraer los argumentos de NavBackStackEntry
que está disponible en la expresión lambda de la función composable()
.
composable("profile/{userId}") { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Para pasar el argumento al destino, debes agregarlo a la ruta cuando realices la llamada a navigate
:
navController.navigate("profile/user1234")
Para obtener una lista de tipos admitidos, consulta Cómo pasar datos entre destinos.
Recupera datos complejos durante la navegación
Se recomienda no pasar objetos de datos complejos cuando navegas, sino pasar la información mínima necesaria, como un identificador único o alguna otra forma de ID, como argumentos, cuando se realizan acciones de navegación:
// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")
Los objetos complejos deben almacenarse como datos en una única fuente de información, como el siguiente:
la capa de datos. Una vez que llegues a tu destino después de navegar, podrás cargar la información requerida desde la única fuente de confianza mediante el ID pasado. Para recuperar los argumentos de tu ViewModel
que son responsables de
Para acceder a la capa de datos, usa el SavedStateHandle
de ViewModel
:
class UserViewModel(
savedStateHandle: SavedStateHandle,
private val userInfoRepository: UserInfoRepository
) : ViewModel() {
private val userId: String = checkNotNull(savedStateHandle["userId"])
// Fetch the relevant user information from the data layer,
// ie. userInfoRepository, based on the passed userId argument
private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)
// …
}
Este enfoque ayuda a evitar la pérdida de datos durante los cambios de configuración y cualquier incoherencia cuando el objeto en cuestión se actualiza o muta.
Para obtener una explicación más detallada sobre por qué deberías evitar pasar datos complejos como argumentos, así como una lista de los tipos de argumentos admitidos, consulta Cómo pasar datos entre destinos.
Cómo agregar argumentos opcionales
La composición de Navigation también admite argumentos de navegación opcionales. Los argumentos opcionales se diferencian de los obligatorios de dos maneras:
- Se deben incluir con la sintaxis del parámetro de búsqueda (
"?argName={argName}"
). - Deben tener un valor
defaultValue
establecido onullable = true
(que configura implícitamente el valor predeterminado ennull
).
Eso significa que todos los argumentos opcionales se deben agregar de forma explícita a la función composable()
de una lista:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
Ahora, incluso si no se pasa ningún argumento al destino, se usa defaultValue
, "user1234".
La estructura de controlar los argumentos a través de las rutas significa que los elementos componibles siguen siendo completamente independientes de Navigation y son mucho más fáciles de probar.
Vínculos directos
Navigation Compose admite vínculos directos implícitos que también se pueden definir como parte de la función composable()
. Su parámetro deepLinks
acepta una lista de
NavDeepLink
que pueden crearse rápidamente con el
Método navDeepLink()
:
val uri = "https://www.example.com"
composable(
"profile?id={id}",
deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("id"))
}
Estos vínculos directos te permiten asociar una URL, una acción o un tipo de MIME específico con una
componible. De forma predeterminada, esos vínculos directos no se exponen a aplicaciones externas. Para
para que estos vínculos directos
estén disponibles externamente, debes agregar
Elementos <intent-filter>
al archivo manifest.xml
de tu app. Para habilitar la configuración
del ejemplo anterior, debes agregar lo siguiente dentro del
<activity>
del manifiesto:
<activity …>
<intent-filter>
...
<data android:scheme="https" android:host="www.example.com" />
</intent-filter>
</activity>
La navegación incluye vínculos directos automáticamente a ese elemento componible cuando otra app activa el vínculo directo.
Esos vínculos directos también se pueden usar para compilar un PendingIntent
con el vínculo apropiado de un elemento componible:
val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
Intent.ACTION_VIEW,
"https://www.example.com/$id".toUri(),
context,
MyActivity::class.java
)
val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(deepLinkIntent)
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
Luego, puedes usar este deepLinkPendingIntent
como cualquier otro PendingIntent
para abrir tu app en el destino del vínculo directo.
Navegación anidada
Para obtener información sobre cómo crear gráficos de navegación anidados, consulta Gráficos anidados.
Integración con la barra de navegación inferior
Si defines NavController
en un nivel superior en tu jerarquía que admite composición, puedes conectar Navigation con otros componentes, como el componente de navegación inferior. Esto te permite navegar
seleccionando los íconos de la parte inferior
.
Si quieres usar los componentes BottomNavigation
y BottomNavigationItem
, agrega la dependencia androidx.compose.material
a tu aplicación para Android.
Groovy
dependencies { implementation "androidx.compose.material:material:1.7.1" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.7.1") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Para vincular los elementos de una barra de navegación inferior a las rutas del gráfico de navegación, se recomienda definir una clase sellada, como Screen
, que se muestra aquí, que contenga la ruta y el ID de recurso de strings de los destinos.
sealed class Screen(val route: String, @StringRes val resourceId: Int) {
object Profile : Screen("profile", R.string.profile)
object FriendsList : Screen("friendslist", R.string.friends_list)
}
Luego, coloca esos elementos en una lista que pueda usar el BottomNavigationItem
:
val items = listOf(
Screen.Profile,
Screen.FriendsList,
)
En el elemento BottomNavigation
componible, obtén el NavBackStackEntry
actual con la función currentBackStackEntryAsState()
. Esta entrada te da acceso al NavDestination
actual. El estado seleccionado de cada
Luego, se puede determinar BottomNavigationItem
comparando la ruta del elemento.
con la ruta del destino actual y sus destinos superiores a
manejar los casos cuando uses la navegación anidada con la
Jerarquía de NavDestination
.
La ruta del elemento también se usa para conectar el valor lambda onClick
a una llamada a navigate
para que cuando se presione en ese elemento se navegue a él. Cuando usas las marcas saveState
y restoreState
, el estado y la pila de actividades de ese elemento se guardan y restablecen de forma correcta a medida que alternas entre los elementos de navegación inferiores.
val navController = rememberNavController()
Scaffold(
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
) { innerPadding ->
NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
composable(Screen.Profile.route) { Profile(navController) }
composable(Screen.FriendsList.route) { FriendsList(navController) }
}
}
Aquí, aprovechas el método NavController.currentBackStackEntryAsState()
para elevar el estado navController
fuera de la función NavHost
y compartirlo con el componente BottomNavigation
. Eso significa que la BottomNavigation
tiene el estado más actualizado automáticamente.
Seguridad de tipos en Navigation Compose
El código de esta página no tiene seguridad de tipos. Puedes llamar al navigate()
con rutas inexistentes o argumentos incorrectos. Sin embargo, puedes
estructurar tu código de navegación para que tenga seguridad de tipos en el entorno de ejecución De esta manera, puedes evitar fallas y asegurarte de lo siguiente:
- Los argumentos que proporcionas cuando navegas a un destino o gráfico de navegación son del tipo correcto y todos los argumentos necesarios están presentes.
- Los argumentos que recuperas de
SavedStateHandle
son del tipo correcto.
Para obtener más información sobre esto, consulta Seguridad de tipos en la DSL de Kotlin y Navigation Compose
Interoperabilidad
Si quieres usar el componente Navigation con Compose, tienes dos opciones:
- Define un gráfico de navegación con el componente Navigation para fragmentos.
- Usa destinos de Compose para definir un gráfico de navegación con un
NavHost
en Compose. Esto será posible solo si todas las pantallas del gráfico de navegación son elementos componibles.
Por lo tanto, para apps híbridas de Compose y Views, se recomienda utilizar el componente Navigation basado en fragmentos. Los fragmentos mantendrán datos basados en objetos View pantallas, pantallas de Compose y pantallas que usan Views y Compose. Una vez cada El contenido de Fragment está en Compose; el siguiente paso es vincular todas esas pantallas junto con Navigation Compose y quitar todos los fragmentos.
Cómo navegar desde Compose con Navigation para fragmentos
A fin de cambiar los destinos dentro del código de Compose, debes exponer eventos que se puedan pasar a cualquier elemento componible de la jerarquía y que este último sea capaz de activarlos:
@Composable
fun MyScreen(onNavigate: (Int) -> Unit) {
Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}
En tu fragmento, para trazar el puente entre Compose y el componente Navigation basado en fragmentos, busca NavController
y navega al destino:
override fun onCreateView( /* ... */ ) {
setContent {
MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
}
}
De manera alternativa, puedes pasar el NavController
en tu jerarquía de Compose.
Sin embargo, exponer funciones simples te dará una posibilidad mucho mayor de probar y volver a usar este método.
Prueba
Separa el código de navegación de los destinos componibles para habilitar las pruebas.
cada elemento componible de forma aislada, separado del elemento NavHost
componible
Esto significa que no debes pasar el navController
directamente a ningún
componible y, en su lugar, pasan devoluciones de llamada de navegación como parámetros. Esto permite
que todos tus elementos componibles se puedan probar de forma individual, ya que no requieren una
instancia de navController
en las pruebas.
El nivel de indirección proporcionado por la lambda composable
es lo que te permite
separar tu código de Navigation del mismo elemento componible. Eso funciona de dos formas:
- Pasa argumentos analizados al elemento que admite composición.
- Pasa expresiones lambdas que deban activarse con el elemento que admita composición para navegar, en lugar del
NavController
.
Por ejemplo, un elemento Profile
componible que toma un elemento userId
como entrada y permite
Es posible que los usuarios que visiten la página de perfil de un amigo tengan la firma de:
@Composable
fun Profile(
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit
) {
…
}
De esta manera, el elemento Profile
componible funciona independientemente de Navigation, lo que permite que se pruebe de forma independiente. La expresión lambda composable
encapsula la lógica mínima necesaria para cerrar la brecha entre las APIs de Navigation y el elemento componible:
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
navController.navigate("profile?userId=$friendUserId")
}
}
Se recomienda escribir pruebas que cubran los requisitos de navegación de la app con pruebas en NavHost
, acciones de navegación que se pasan a los elementos componibles, así como a los elementos individuales de la pantalla.
Prueba el elemento NavHost
Para comenzar a probar tu NavHost
, agrega la siguiente prueba de navegación
dependencia:
dependencies {
// ...
androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
// ...
}
Puedes configurar el sujeto de prueba NavHost
y pasarle una instancia de la instancia navController
. Para ello, la navegación
El artefacto de prueba proporciona un TestNavHostController
. Una prueba de la IU que verifique el destino de inicio de tu app y NavHost
tendría el siguiente aspecto:
class NavigationTest {
@get:Rule
val composeTestRule = createComposeRule()
lateinit var navController: TestNavHostController
@Before
fun setupAppNavHost() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
AppNavHost(navController = navController)
}
}
// Unit test
@Test
fun appNavHost_verifyStartDestination() {
composeTestRule
.onNodeWithContentDescription("Start Screen")
.assertIsDisplayed()
}
}
Cómo probar las acciones de navegación
Puedes probar tu implementación de navegación de varias maneras. Para ello, haz clic en los elementos de la IU y, luego, verifica el destino que se muestra o compara la ruta esperada con la ruta real.
Dado que deseas probar la implementación concreta de tu app, es preferible que hagas clic en la IU. Para aprender a probar esto junto con las funciones individuales de componibilidad por separado, asegúrate de consultar el codelab Pruebas en Jetpack Compose.
También puedes usar navController
para verificar tus aserciones comparando la ruta de string actual con la esperada, mediante el currentBackStackEntry
de navController
:
@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
composeTestRule.onNodeWithContentDescription("All Profiles")
.performScrollTo()
.performClick()
val route = navController.currentBackStackEntry?.destination?.route
assertEquals(route, "profiles")
}
Para obtener más información sobre los conceptos básicos de las pruebas de Compose, consulta lo siguiente: Cómo probar tu diseño de Compose y las pruebas en Jetpack Compose en este codelab. Si deseas obtener más información sobre las pruebas avanzadas del código de navegación, consulta la guía para Probar Navigation.
Más información
Para obtener más información sobre Jetpack Navigation, consulta Cómo comenzar con el componente de Navigation o el codelab de Navigation de Jetpack Compose.
Si deseas obtener información para diseñar la navegación de tu app, de modo que se adapte a diferentes tamaños de pantalla, orientaciones y factores de forma, consulta el artículo sobre Navigation para IU responsivas.
Para obtener información sobre una implementación más avanzada de la navegación de Compose en un app modularizada con conceptos como gráficos anidados y barra de navegación inferior consulta la app de Now in Android en GitHub.
Ejemplos
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Material Design 2 en Compose
- Cómo migrar Jetpack Navigation a Navigation Compose
- Dónde elevar el estado