Navigation 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.

Para obtener la biblioteca de navegación alfa más reciente compilada específicamente para Compose, consulta la documentación de Navigation 3.

Configuración

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

Groovy

dependencies {
    def nav_version = "2.9.4"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.9.4"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

Comenzar

Cuando implementes la navegación en una app, implementa un host, un gráfico y un controlador de navegación. Para obtener más información, consulta la descripción general de Navigation.

Para obtener información sobre cómo crear un NavController en Compose, consulta la sección de Compose en Cómo crear un controlador de navegación.

Cómo crear un NavHost

Para obtener información sobre cómo crear un NavHost en Compose, consulta la sección de Compose en Cómo diseñar tu gráfico de navegación.

Para obtener información sobre cómo navegar a un elemento componible, consulta Cómo navegar a un destino en la documentación de la arquitectura.

Para obtener información sobre cómo pasar argumentos entre destinos componibles, consulta la sección de Compose en Cómo diseñar tu gráfico de navegación.

Cómo recuperar 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(id = "user1234"))

Los objetos complejos se deben almacenar como datos en una sola fuente de información, como 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 en tu ViewModel que es responsable de acceder a la capa de datos, usa el SavedStateHandle del ViewModel:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val profile = savedStateHandle.toRoute<Profile>()

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id)

// …

}

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.

Navigation Compose admite vínculos directos que también se pueden definir como parte de la función composable(). Su parámetro deepLinks acepta una lista de objetos NavDeepLink que se pueden crear con rapidez usando el método navDeepLink():

@Serializable data class Profile(val id: String)
val uri = "https://www.example.com"

composable<Profile>(
  deepLinks = listOf(
    navDeepLink<Profile>(basePath = "$uri/profile")
  )
) { backStackEntry ->
  ProfileScreen(id = backStackEntry.toRoute<Profile>().id)
}

Esos vínculos directos te permiten asociar una URL, acción o tipo de MIME específico con un elemento componible. 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 en el ejemplo 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 incluye automáticamente vínculos directos 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 que admite composición:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/profile/$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.

Compila una barra de navegación inferior y un riel de navegación adaptables

El NavigationSuiteScaffold muestra la IU de navegación adecuada según el WindowSizeClass en el que se renderiza tu app. En pantallas compactas, el NavigationSuiteScaffold muestra una barra de navegación inferior; en una pantalla expandida, se muestra un riel de navegación.

Consulta Cómo compilar una navegación adaptable para obtener más información.

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 conservarán las pantallas basadas en Views, pantallas de Compose y pantallas que usan tanto Views como Compose. Una vez que el contenido de cada fragmento esté en Compose, el siguiente paso es vincular todas esas pantallas con Navigation Compose y quitar todos los 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 tus destinos componibles para habilitar la prueba de cada elemento componible de forma aislada, separada del elemento componible NavHost.

Esto significa que no debes pasar el componente navController directamente a ningún elemento componible y, en su lugar, pasar devoluciones de llamada de navegación como parámetros. De esta manera, todos tus elementos componibles se pueden probar de forma individual, ya que no requieren una instancia de navController en las pruebas.

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 componible. Eso funciona de dos formas:

  • Pasa argumentos analizados al elemento que admite composición.
  • Pasa expresiones lambda que deban activarse con el elemento componible para navegar, en lugar del NavController.

Por ejemplo, un elemento ProfileScreen componible 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 ProfileScreen(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 
}

De esta manera, el elemento ProfileScreen 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 API de Navigation y el elemento que admite composición:

@Serializable data class Profile(id: String)

composable<Profile> { backStackEntry ->
    val profile = backStackEntry.toRoute<Profile>()
    ProfileScreen(userId = profile.id) { friendUserId ->
        navController.navigate(route = Profile(id = 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 dependencia de navegación-prueba:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

Encapsula el NavHost de tu app en un elemento componible que acepte un NavHostController como parámetro.

@Composable
fun AppNavHost(navController: NavHostController){
  NavHost(navController = navController){ ... }
}

Ahora puedes probar AppNavHost y toda la lógica de navegación definida dentro de NavHost pasando una instancia del artefacto de prueba de navegación TestNavHostController. Una prueba de 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.

Como 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 actual con la esperada, mediante el currentBackStackEntry de navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false)
}

Para obtener más orientación sobre los conceptos básicos de las pruebas de Compose, consulta Cómo probar tu diseño de Compose y el codelab Pruebas en Jetpack Compose. 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 Navegación para IU responsivas.

Para obtener información sobre una implementación más avanzada de la navegación de Compose en una app modularizada, incluidos conceptos como gráficos anidados y la integración de la barra de navegación inferior, consulta la app de Now in Android en GitHub.

Ejemplos