Cómo navegar con Compose

El componente Navigation proporciona compatibilidad con aplicaciones de Jetpack Compose. Puedes navegar entre los elementos que admiten composición y aprovechar la infraestructura y las funciones del componente Navigation.

Configuración

Para admitir Compose, usa la siguiente dependencia en el archivo build.gradle del módulo de tu app:

dependencies {
  implementation "androidx.navigation:navigation-compose:1.0.0-alpha06"
}

Cómo comenzar

NavController es la API central del componente Navigation. Tiene estado y realiza un seguimiento de la pila de actividades de elementos que admiten composición que conforman las pantallas de tu app y el estado de cada pantalla.

Puedes crear un objeto NavController con el método rememberNavController() en el elemento que admite composición:

val navController = rememberNavController()

Debes crear el NavController en el lugar de la jerarquía correspondiente que sea accesible para todos los elementos que admitan composición y necesiten hacer referencia a él. Eso sigue los principios de la elevación de estado y te permite usar el NavController y el estado que proporciona a través de currentBackStackEntryAsState() como fuente de confianza para actualizar elementos componibles fuera de tus pantallas. Consulta Integración con la barra de navegación inferior para ver un ejemplo de esta funcionalidad.

Cómo crear un NavHost

Cada NavController debe estar asociado con un único elemento NavHost que admita composición. El NavHost vincula el NavController con un gráfico de navegación que especifica los destinos componibles que deberías poder navegar. A medida que navegas por los elementos que admiten composición, el contenido del NavHost se reescribe automáticamente. Cada destino que admite composición en tu gráfico de navegación está asociado a una ruta.

Para crear el NavHost, se requiere el objeto NavController que se creó antes a través de rememberNavController() y la ruta del destino inicial de tu gráfico. La creación de NavHost usa la sintaxis lambda del DSL de Kotlin de Navigation para construir el gráfico de navegación. Puedes agregar el elemento a tu estructura de navegación con el método composable(), que requiere que proporciones una ruta y el elemento que admite composición que debe vincularse al destino:

NavHost(navController, startDestination = "profile") {
    composable("profile") { Profile(...) }
    composable("friendslist") { FriendsList(...) }
    ...
}

Para navegar a un destino que admite composición en el gráfico de navegación, debes usar el método navigate(). navigate() toma un solo parámetro String que representa la ruta del destino. Para navegar desde un elemento que admite composición dentro del gráfico de navegación, llama a navigate():

fun Profile(navController: NavController) {
    ...
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    ...
}

Solo debes llamar a navigate() como parte de una devolución de llamada, y no como parte de la composición, para evitar llamar a navigate() en cada recomposición.

De forma predeterminada, navigate() agrega el nuevo destino a la pila de actividades. Puedes modificar el comportamiento de navigate adjuntando opciones de navegación adicionales a nuestra llamada a navigate() de la siguiente manera:

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friends" destination
navController.navigate(“friends”) {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friends" destination
navController.navigate("friends") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

Consulta la guía de popUpTo para ver más casos de uso.

La composición de Navigation 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 a cómo agregas 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. Puedes especificar otro tipo con el parámetro arguments para configurar un type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Debes extraer los NavArguments de la 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 agregar el valor a la ruta en lugar del marcador de posición en la llamada a navigate:

navController.navigate("profile/user1234")

Para obtener una lista de tipos 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 mediante la sintaxis del parámetro de búsqueda ("?argName={argName}").
  • Deben tener un valor defaultValue establecido o nullability = true (que configura implícitamente el valor predeterminado en null).

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 = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Ahora, incluso si no se pasa ningún argumento al destino, se usará el valor defaultValue de "me" en su lugar.

La estructura de controlar los argumentos a través de las rutas significa que los elementos que admiten composición siguen siendo completamente independientes de Navigation y son mucho más fáciles de probar.

Navigation Compose admite vínculos directos implícitos que también se pueden definir como parte de la función composable(). Agrégalos en una lista con navDeepLink():

val uri = "https://example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Esos vínculos directos te permiten asociar una URL, acción o tipo de MIME específico con un elemento que admite composición. De forma predeterminada, esos vínculos directos no se exponen a aplicaciones externas. Para que los vínculos directos estén disponibles externamente, debes agregar los elementos <intent-filter> apropiados al archivo manifest.xml de tu app. Para habilitar el vínculo directo anterior, debes agregar lo siguiente dentro del elemento <activity> del manifiesto:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

Navigation vinculará automáticamente el vínculo directo a ese elemento componible cuando otra app active el vínculo en cuestión.

Esos vínculos directos también se pueden usar para compilar un PendingIntent con el vínculo apropiado de un elemento que admite composición:

val id = "exampleId"
val context = AmbientContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://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

Los destinos se pueden agrupar en un gráfico anidado para modularizar un flujo en particular de la IU de tu app. Un ejemplo de esto podría ser un flujo de acceso independiente.

El gráfico anidado encapsula sus destinos. Al igual que sucede con el gráfico raíz, un gráfico anidado debe tener un destino que su ruta identifique como el destino de inicio. Este es el destino al que se navega cuando navegas a la ruta asociada con el gráfico anidado.

Para agregar un gráfico anidado a tu NavHost, puedes usar la función de extensión navigation como se muestra a continuación:

NavHost(navController, startDestination = startRoute) {
    ...
    navigation(startDestination = nestedStartRoute, route = nested) {
        composable(nestedStartRoute) { ... }
    }
    ...
}

Integración con la barra de navegación inferior

Si defines el NavController en un nivel superior de tu jerarquía que admite composición, puedes conectar Navigation con otros componentes, como la BottomNavBar. Eso te permite navegar seleccionando los íconos de la barra inferior.

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 que admite composición, obtén la NavBackStackEntry mediante la función currentBackStackEntryAsState() y, con la entrada, recupera la ruta de los argumentos con la constante KEY_ROUTE que es parte de NavHostController. Con la ruta, determina si el elemento seleccionado es el destino actual. Luego, responde adecuadamente configurando la etiqueta, resaltando el elemento y navegando si las rutas no coinciden.

val navController = rememberNavController()
Scaffold(
    bottomBar = {
        BottomNavigation {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
            items.forEach { screen ->
                BottomNavigationItem(
                    icon = { Icon(Icons.Filled.Favorite) },
                    label = { Text(stringResource(screen.resourceId)) },
                    selected = currentRoute == screen.route,
                    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.startDestination
                            // Avoid multiple copies of the same destination when
                            // reselecting the same item
                            launchSingleTop = true
                        }
                    }
                )
            }
        }
    }
) {

    NavHost(navController, startDestination = Screen.Profile.route) {
        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.

Pruebas

Te recomendamos separar el código de Navigation de los destinos componibles para habilitar la prueba de cada elemento que admite composición de forma independiente del elemento NavHost componible.

El nivel de indirección proporcionado por la expresión lambda composable es lo que te permite separar el código de Navigation del mismo elemento que admite composición. 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 que admite composición que recibe un userId como entrada y permite a los usuarios navegar a la página de perfil de un amigo podría tener la firma de:


@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

Aquí vemos que el elemento Profile que admite composición funciona independientemente de Navigation, lo que permite probarlo de manera independiente. La expresión lambda composable encapsula la lógica mínima necesaria para cerrar la brecha entre las API de Navigation y el elemento que admite composición:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
}

Más información

Para obtener más información sobre Jetpack Navigation, consulta Cómo comenzar a usar el componente Navigation.