Navigation de Jetpack Compose

Última actualización: 17/03/2021

Requisitos

Navigation es una biblioteca de Jetpack que permite navegar de un destino dentro de tu app a otro, por rutas específicas. La biblioteca Navigation también proporciona un artefacto específico para permitir una navegación idiomática y coherente dentro de las pantallas que se compilan con Jetpack Compose. Este artefacto (navigation-compose) es el punto central de este codelab.

Actividades

Usarás el estudio de Material sobre Rally como base para este codelab. Migrarás el código de navegación existente para usar el componente Navigation de Jetpack a fin de navegar entre pantallas en Jetpack Compose.

Qué aprenderás

  • Aspectos básicos sobre el uso de Navigation de Jetpack con Jetpack Compose
  • Cómo navegar entre elementos que admiten composición
  • Cómo navegar con argumentos obligatorios y opcionales
  • Cómo navegar con vínculos directos
  • Cómo integrar TabBar en tu jerarquía de navegación
  • Cómo probar Navigation

Puedes continuar este codelab en tu máquina.

Para continuar por tu cuenta, clona el punto de partida del codelab.

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

Como alternativa, puedes descargar dos archivos ZIP:

Ahora que descargaste el código, abre el proyecto NavigationCodelab en Android Studio. Ya estás listo para comenzar.

Rally es una app existente que, en un principio, no utiliza Navigation. La migración consiste en varios pasos:

  1. Agregar la dependencia Navigation
  2. Configurar NavController y NavHost
  3. Preparar rutas para destinos
  4. Reemplazar el mecanismo original de destino por rutas de navegación

Cómo agregar la dependencia Navigation

Abre el archivo de compilación de la app, que se encuentra en app/build.gradle. En la sección de dependencias, agrega la dependencia navigation-compose.

dependencies {
  implementation "androidx.navigation:navigation-compose:2.4.0-alpha01"
  // other dependencies
}

Ahora, sincroniza el proyecto, y estarás listo para comenzar a usar Navigation en Compose.

Cómo configurar el componente NavController

NavController es el componente central cuando se usa Navigation en Compose; permite realizar un seguimiento de las entradas de la pila de actividades, adelantar la pila, habilitar la manipulación de la pila de actividades y navegar entre los estados de la pantalla. Como NavController es fundamental para la navegación, se debe crear primero a fin de poder navegar a los destinos.

En Compose, trabajas con un objeto NavHostController, que es una subclase de NavController. Obtén un componente NavController con la función rememberNavController(); esta función crea y recuerda un componente NavController que permanece vigente frente a los cambios de configuración (mediante rememberSavable). El componente NavController está asociado a un solo elemento NavHost que admite composición. El elemento NavHost vincula el componente NavController con un gráfico de navegación en el que se especifican los destinos que admiten composición.

Para este codelab, obtén y almacena el componente NavController en RallyApp. Es la raíz que admite composición para toda la aplicación. Puede encontrar este componente en el archivo RallyActivity.kt.

import androidx.navigation.compose.rememberNavController
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
        val navController = rememberNavController()
        Scaffold(...
}

Cómo preparar rutas para destinos

Descripción general

La app Rally tiene tres pantallas:

  1. Overview: Descripción general de todas las alertas y transacciones financieras
  2. Accounts: Estadísticas sobre las cuentas existentes
  3. Bills: Gastos programados

85ac26dfa57b9fe9.png a7c8a51fe2503409.png 9e4c38a6bff0fdbb.png

Las tres pantallas se compilan con elementos que admiten composición. Explora el archivo RallyScreen.kt. Las tres pantallas se declaran en este archivo. Más adelante, asignarás estas pantallas a los destinos de navegación, con Overview como destino de inicio. También, quitarás los elementos que admiten composición del objeto RallyScreen y los moverás a un elemento NavHost. Por ahora, puedes dejar RallyScreen intacto.

Cuando usas Navigation dentro de Compose, las rutas se representan como strings. Puedes pensar que estas strings son similares a URL o vínculos directos. En este codelab, usaremos la propiedad name de cada objeto RallyScreen como la ruta, por ejemplo, RallyScreen.Overview.name.

Preparación

Regresa al elemento RallyApp que admite composición en RallyActivity.kt y reemplaza el objeto Box con el contenido de la pantalla por un elemento NavHost que recién creaste. Pasa el componente navController que creamos en el paso anterior. El elemento NavHost también necesita un objeto startDestination. Establécelo en RallyScreen.Overview.name. Además, crea un objeto Modifier para pasar el padding al elemento NavHost.

import androidx.compose.foundation.layout.Box
import androidx.compose.material.Scaffold
import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = RallyScreen.Overview.name
            modifier = Modifier.padding(innerPadding)
        ) { ... }

Ahora, podemos definir nuestro gráfico de navegación. Los destinos a los que NavHost puede navegar ya pueden aceptar destinos. Para ello, usamos un elemento NavGraphBuilder, que se le brinda al último parámetro de NavHost: una expresión lambda que se usa con el objeto de definir tu gráfico. Como este parámetro espera una función, puedes declarar destinos en una expresión lambda final. El artefacto Navigation de Compose brinda la función de extensión NavGraphBuilder.composable. Úsala para definir los destinos de navegación en el gráfico.

import androidx.navigation.compose.NavHost
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)

) {
    composable(RallyScreen.Overview.name) { ... }
}

Por ahora, configuraremos, de manera temporal, un elemento Text con el nombre de la pantalla como contenido del elemento que admite composición. En el siguiente paso, usaremos los elementos existentes que admiten composición.

import androidx.compose.material.Text
import androidx.navigation.compose.composable
...

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(text = RallyScreen.Overview.name)
    }

    // TODO: Add the other two screens
}

Ahora, quita la llamada currentScreen.content y ejecuta la app; verás el nombre del destino de inicio y las pestañas que se mostraron anteriormente.

Deberías terminar con un elemento NavHost similar al siguiente:

NavHost(
    navController = navController,
    startDestination = RallyScreen.Overview.name
    modifier = Modifier.padding(innerPadding)
) {
    composable(RallyScreen.Overview.name) {
      Text(RallyScreen.Overview.name)
    }
    composable(RallyScreen.Accounts.name) {
        Text(RallyScreen.Accounts.name)
    }
    composable(RallyScreen.Bills.name) {
        Text(RallyScreen.Bills.name)
    }
}

Ahora, el elemento NavHost puede reemplazar el objeto Box dentro de Scaffold. Pasa el objeto Modifier a NavHost para mantener el elemento innerPadding intacto.

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen -> currentScreen = screen },
                    currentScreen = currentScreen
                )
            }
        ) { innerPadding ->
            NavHost(
                navController = navController,
                startDestination = RallyScreen.Overview.name
                modifier = Modifier.padding(innerPadding)) {
            }
        }
    }
}

En este punto, la barra superior todavía no está conectada, por lo que hacer clic en las pestañas no cambiará el elemento que se muestra y admite composición. En el siguiente paso, te ocuparás de esta tarea.

Cómo integrar, por completo, los cambios de estado de la barra de navegación

En este paso, conectarás el elemento RallyTabRow y borrarás el código de navegación manual y actual. Después de que termines este paso, el componente Navigation se encargará, por completo, del enrutamiento.

Todavía en RallyActivity, observarás que el elemento RallyTabRow que admite composición tiene una devolución de llamada cuando se hace clic en una pestaña, que se llama onTabSelected. Actualiza el código de selección para usar el componente navController a fin de navegar a la pantalla seleccionada.

Es todo lo que necesitas para navegar a una pantalla mediante el elemento TabRow con Navigation:

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        // FIXME: This duplicate source of truth
        //  will be removed later.
        var currentScreen by rememberSaveable {
            mutableStateOf(RallyScreen.Overview)
        }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = allScreens,
                    onTabSelected = { screen ->
                        navController.navigate(screen.name)
                },
                    currentScreen = currentScreen,
                )
            }

Con este cambio, la propiedad currentScreen ya no se actualizará, lo que implica que los elementos seleccionados no se expandirán ni se contraerán. Para volver a habilitar este comportamiento, también se debe actualizar la propiedad currentScreen. Afortunadamente, Navigation conserva la pila de actividades para ti y puede brindarte la entrada actual de la pila de actividades como un elemento State. Con este elemento State, puedes reaccionar a los cambios en la pila de actividades. Incluso, puedes consultar la entrada actual de la pila de actividades para su ruta.

A fin de terminar de migrar la selección de pantalla del elemento TabRow a Navigation, actualiza la propiedad currentScreen para usar la pila de actividades de navegación de la siguiente manera.

import androidx.navigation.compose.currentBackStackEntryAsState
...

@Composable
fun RallyApp() {
    RallyTheme {
        val allScreens = RallyScreen.values().toList()
        val navController = rememberNavController()
        val backstackEntry = navController.currentBackStackEntryAsState()
        val currentScreen = RallyScreen.fromRoute(
            backstackEntry.value?.destination?.route
        )
        ...
    }
}

En este punto, cuando ejecutas la app, puedes cambiar entre las pantallas con las pestañas, pero todo lo que se muestra es el nombre de la pantalla. Antes de que se pueda mostrar la pantalla, se debe migrar el objeto RallyScreen a Navigation.

Cómo migrar RallyScreen a Navigation

Después de completar este paso, el elemento que admite composición se desvinculará, por completo, de la enumeración RallyScreen y se moverá a NavHost. RallyScreen solo existirá con el fin de brindar un ícono y un título para la pantalla.

Abre RallyScreen.kt. Mueve la implementación de body de cada pantalla a los elementos correspondientes que admitan composición dentro del elemento NavHost en RallyApp.

import com.example.compose.rally.data.UserData
import com.example.compose.rally.ui.accounts.AccountsBody
import com.example.compose.rally.ui.bills.BillsBody
import com.example.compose.rally.ui.overview.OverviewBody
...

NavHost(
    navController = navController,
    startDestination = Overview.name
) {

    composable(Overview.name) {
        OverviewBody()
    }
    composable(Accounts.name) {
        AccountsBody(accounts = UserData.accounts)
    }
    composable(Bills.name) {
        BillsBody(bills = UserData.bills)
    }
}

En este punto, puedes quitar, de forma segura, la función content, el parámetro body y sus usos de RallyScreen. Por lo tanto, el código se verá de la siguiente manera:

enum class RallyScreen(
    val icon: ImageVector,
) {
    Overview(
        icon = Icons.Filled.PieChart,
    ),
    Accounts(
        icon = Icons.Filled.AttachMoney,
    ),
    Bills(
        icon = Icons.Filled.MoneyOff,
    );

    companion object {
        ...
    }
}

Vuelve a ejecutar la app. Observarás las tres pantallas originales y podrás navegar entre ellas mediante el elemento TabRow con clics en los botones "see all".

Cómo habilitar clics en OverviewScreen

En este codelab, los eventos de clic en el objeto OverviewBody se ignoraron en un principio. Por lo tanto, podrías hacer clic en el botón "SEE ALL", pero no te dirigía a ningún lugar.

1d103795c7a683b8.gif

Sin embargo, podemos solucionarlo.

El objeto OverviewBody puede aceptar varias funciones como devoluciones de llamada para eventos de clic. Implementemos onClickSeeAllAccounts y onClickSeeAllBills para navegar a destinos relevantes.

Para habilitar la navegación cuando se haga clic en el botón "see all", usa el componente navController y navega a la pantalla Accounts o Bills. Abre RallyActivity.kt, busca el objeto OverviewBody en NavHost y agrega las llamadas de navegación.

OverviewBody(
    onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
    onClickSeeAllBills = { navController.navigate(Bills.name) },
)

Ahora, puedes cambiar, con facilidad, el comportamiento de los eventos de clic para OverviewBody. Mantener el componente navController en el nivel superior de tu jerarquía de navegación y no pasarlo directamente al objeto OverviewBody facilita obtener una vista previa de OverviewBody o realizar, de forma aislada, pruebas de este objeto, sin tener que depender de que un componente navController real esté presente cuando realices esta acción.

Agreguemos algunas funcionalidades nuevas a Rally. Agregaremos una pantalla Accounts que muestra los detalles de una cuenta individual cuando se hace clic en una fila.

Un argumento de navegación permite que la ruta sea dinámica. Los argumentos de navegación son una herramienta muy potente que permite que el comportamiento del enrutamiento sea dinámico. Para ello, pasa uno o más argumentos a una ruta y ajusta los tipos de argumentos o los valores predeterminados.

En el objeto RallyActivity, agrega un destino nuevo al gráfico. Para ello, agrega un elemento nuevo que admita composición al elemento NavHost existente con el argumento Accounts/{name}. Para este destino, también especificaremos una lista de objetos navArgument. Definiremos un argumento único con el nombre "name" del tipo String.

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name

composable(
    route = "$accountsName/{name}",
    arguments = listOf(
        navArgument("name") {
            // Make argument type safe
            type = NavType.StringType
        }
    )
) {
    // TODO
}

El cuerpo de cada destino composable recibe un parámetro (que, hasta ahora, no usamos) del elemento NavBackStackEntry actual que le da forma a la ruta y a los argumentos del destino actual. Podemos usar arguments para recuperar el argumento, es decir, el nombre de la cuenta seleccionada, buscarlo en UserData y pasarlo al elemento SingleAccountBody que admite composición.

También, puedes brindar un valor predeterminado para usar si no se proporcionó el argumento. Omitiremos este paso porque no es necesario aquí.

Tu código debería verse de la siguiente manera:

import androidx.navigation.NavType
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navArgument
...

val accountsName = RallyScreen.Accounts.name
NavHost(...) {
    ...
    composable(
        "$accountsName/{name}",
        arguments = listOf(
            navArgument("name") {
                // Make argument type safe
                type = NavType.StringType
            }
        )
    ) { entry -> // Look up "name" in NavBackStackEntry's arguments
        val accountName = entry.arguments?.getString("name")
        // Find first name match in UserData
        val account = UserData.getAccount(accountName)
        // Pass account to SingleAccountBody
        SingleAccountBody(account = account)
    }
}

Ahora que el elemento que admite composición ya se configuró con el argumento, puedes navegar a este con el componente navController de la siguiente manera: navController.navigate("${RallyScreen.Accounts.name}/$accountName").

Agrega esta función al parámetro onAccountClick de la declaración del objeto OverviewBody en NavHost y al elemento onAccountClick de AccountsBody.

Para poder volver a usar todo, puedes crear una función auxiliar privada, como se muestra a continuación.

fun RallyNavHost(
    ...
) {
    NavHost(
        ...
    ) {
        composable(Overview.name) {
            OverviewBody(
                ...
                onAccountClick = { name ->
                    navigateToSingleAccount(navController, name)
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navigateToSingleAccount(
                    navController = navController,
                    accountName = name
                )
            }
        }
        ...
    }
}

private fun navigateToSingleAccount(
    navController: NavHostController,
    accountName: String
) {
    navController.navigate("${Accounts.name}/$accountName")
}

En este punto, cuando ejecutes la app, podrás hacer clic en cada cuenta, y te dirigirá a una pantalla en la que se mostrarán los datos de la cuenta determinada.

c78db82ce19ab97c.gif

Además de los argumentos, también puedes utilizar vínculos directos para exponer destinos en tu app a apps de terceros. En esta sección, agregarás un vínculo directo nuevo a la ruta que se creó en la sección anterior, lo que habilitará los vínculos directos desde fuera de tu app hasta cuentas individuales directamente por su nombre.

Cómo agregar el filtro de intents

Para comenzar, agrega el vínculo directo a AndroidManifest.xml. Debes crear un filtro de intents nuevo para el objeto RallyActivity con la acción VIEW, y las categorías BROWSABLE y DEFAULT.

Luego, con la etiqueta data, agrega scheme, host y pathPrefix.

En este codelab, se usará rally://accounts/{name} como una URL de vínculo directo.

No es necesario declarar el argumento "name" en AndroidManifest. Navigation lo analizará como un argumento.

<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="accounts" />
    </intent-filter>
</activity>

Ahora, puedes reaccionar al intent entrante desde el objeto RallyActivity.

El elemento que admite composición y que creaste antes para aceptar argumentos también puede aceptar el vínculo directo que creaste recién.

Agrega una lista de deepLinks con la función navDeepLink. Pasa el elemento uriPattern y brinda el URI coincidente para el objeto intent-filter anterior. Pasa el vínculo directo que creaste al elemento que admite composición con el parámetro deepLinks.

val accountsName = RallyScreen.Accounts.name

composable(
    "${accountsName}/{name}",
    arguments = listOf(
        navArgument("name") {
            type = NavType.StringType
        },
    ),
    deepLinks =  listOf(navDeepLink {
        uriPattern = "rally://$accountsName/{name}"
    })
)

Ahora, tu app está lista para controlar vínculos directos. Para probar que se comporte de manera correcta, instala una versión actual de Rally en un emulador o dispositivo, abre una línea de comandos y ejecuta el siguiente:

adb shell am start -d "rally://accounts/Checking" -a android.intent.action.VIEW

De esta manera, serás dirigido directamente a la cuenta corriente, y funcionará para todos los nombres de cuenta en la app.

Ahora, el elemento NavHost está completo. Puedes extraerlo del elemento RallyApp que admite composición, moverlo a su propia función y llamarlo RallyNavHost. Este es el único elemento que admite composición que debes trabajar directamente con el componente navController. Si no creas el componente navController dentro de RallyNavHost, todavía puedes usarlo para crear la selección de pestaña, que forma parte de la estructura superior, dentro de RallyApp.

@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.name,
        modifier = modifier
    ) {
        composable(Overview.name) {
            OverviewBody(
                onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
                onClickSeeAllBills = { navController.navigate(Bills.name) },
                onAccountClick = { name ->
                    navController.navigate("${Accounts.name}/$name")
                },
            )
        }
        composable(Accounts.name) {
            AccountsBody(accounts = UserData.accounts) { name ->
                navController.navigate("Accounts/${name}")
            }
        }
        composable(Bills.name) {
            BillsBody(bills = UserData.bills)
        }
        val accountsName = Accounts.name
        composable(
            "$accountsName/{name}",
            arguments = listOf(
                navArgument("name") {
                    type = NavType.StringType
                },
            ),
            deepLinks = listOf(navDeepLink {
                uriPattern = "example://rally/$accountsName/{name}"
            }),
        ) { entry ->
            val accountName = entry.arguments?.getString("name")
            val account = UserData.getAccount(accountName)
            SingleAccountBody(account = account)
        }
    }
}

Además, asegúrate de reemplazar el sitio de llamada original por RallyNavHost(navController) para que todo continúe funcionando como se espera.

fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )

        }
     }
}

Desde el comienzo de este codelab, nos aseguramos de no pasar el componente navController directamente a ningún elemento que admita composición. En su lugar, pasaste devoluciones de llamada como parámetros. De esta manera, todos los elementos que admiten composición se pueden probar de forma individual. También puedes probar todo el elemento NavHost, y se tratará este tema en el siguiente paso. Para probar las funciones individuales que admiten composición, asegúrate de consultar el codelab Pruebas en Jetpack Compose.

Cómo preparar la clase de prueba

Se puede probar el elemento NavHost de forma aislada del mismo objeto Activity.

Como esta prueba se seguirá ejecutando en un dispositivo Android, deberás crear tu archivo de prueba en el directorio androidTest debajo de /app/src/androidTest/java/com/example/compose/rally.

Hazlo y asígnale el nombre RallyNavHostTest.

Luego, para usar las API de pruebas de Compose, crea la regla de prueba de Compose, como se muestra a continuación.

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}

Ahora, estás listo para escribir una prueba real.

Cómo escribir tu primera prueba

Crea una función de prueba, que debe ser pública y tener una anotación @Test. En esa función, debes configurar el contenido que deseas probar. Para ello, usa el elemento setContent de composeTestRule. Necesita un parámetro que admite composición, y te permite escribir código de Compose, como si estuvieras en una app normal. Configura el elemento RallyNavHost de la misma manera que hiciste en RallyActivity.

import androidx.navigation.compose.rememberNavController
import org.junit.Assert.fail
import org.junit.Test
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            val navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        fail()
    }
}

Si copiaste el código anterior, la llamada fail() garantizará que la prueba falle hasta que se realice una aserción real. Te servirá como recordatorio para que termines de implementar la prueba.

Puedes verificar que se muestre la pantalla correcta con la descripción de contenido. En este codelab, te brindamos descripciones de contenido para "Accounts Screen" y "Overview Screen" a fin de que puedas usarlas para la verificación de prueba. Crea una propiedad lateinit en la misma clase de prueba, para que también la puedas usar en pruebas futuras.

Para facilitar el inicio, verifica que se muestre OverviewScreen.

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.navigation.NavHostController
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Quita la llamada fail(), vuelve a ejecutar la prueba, y esta se completará. Bien hecho.

En cada una de las siguientes pruebas, RallyNavHost se configurará de la misma manera. Por lo tanto, puedes extraerla en una función con una anotación @Before para mantener limpio tu código.

import org.junit.Before
...

class RallyNavHostTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: NavHostController

    @Before
    fun setupRallyNavHost() {
        composeTestRule.setContent {
            navController = rememberNavController()
            RallyNavHost(navController = navController)
        }
    }

    @Test
    fun rallyNavHost() {
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Puedes probar la implementación de navegación de varias maneras. Para ello, debes hacer clic en los elementos de la IU que deberían dirigirte a un destino nuevo o llamar a navigate con el nombre de ruta correspondiente.

Cómo probar mediante la IU y la regla de prueba

Como deseas probar la implementación de tu app, es preferible que hagas clic en la IU. Escribe una prueba para hacer clic en el botón "All Accounts" que te dirige a la pantalla "Accounts" y verifica que se muestre la pantalla derecha.

import androidx.compose.ui.test.performClick
...

@Test
fun rallyNavHost_navigateToAllAccounts_viaUI() {
    composeTestRule
        .onNodeWithContentDescription("All Accounts")
        .performClick()
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Cómo probar mediante la IU y navController

También puedes usar el complemento navController para verificar tus aserciones. Para ello, haz clic en la IU y, luego, compara la ruta actual con la que esperas, por medio de backstackEntry.value?.destination?.route.

import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
...

@Test
fun rallyNavHost_navigateToBills_viaUI() {
    // When click on "All Bills"
    composeTestRule.onNodeWithContentDescription("All Bills").apply {
        performScrollTo()
        performClick()
    }
    // Then the route is "Bills"
    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "Bills")
}

Cómo probar mediante navController

Una tercera opción es llamar a navController.navigate directamente; sin embargo, para esta opción, hay una advertencia. Las llamadas a navController.navigate deben realizarse en el subproceso de IU. Para lograrlo, usa Coroutines con el despachador de subprocesos Main. Además, como la llamada debe ocurrir antes de poder realizar una aserción sobre un estado nuevo, debes unirla a una llamada runBlocking.

runBlocking {
    withContext(Dispatchers.Main) {
        navController.navigate(RallyScreen.Accounts.name)
    }
}

Con esta llamada, podrás navegar por la app y confirmar que la ruta te dirige a donde esperas.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
...

@Test
fun rallyNavHost_navigateToAllAccounts_callingNavigate() {
    runBlocking {
        withContext(Dispatchers.Main) {
            navController.navigate(RallyScreen.Accounts.name)
        }
    }
    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Para obtener más información sobre las pruebas en Compose, consulta el codelab vinculado en la sección "¿Qué sigue?" del siguiente paso.

¡Felicitaciones! Completaste este codelab con éxito.

Agregaste Navigation a la app de Rally y ya conoces los conceptos clave sobre el uso de Navigation en Jetpack Compose. Aprendiste a crear un gráfico de navegación de destinos que admiten composición, agregaste argumentos a rutas y vínculos directos, y probaste tu implementación de varias maneras.

¿Qué sigue?

Consulta algunos de estos Codelabs…

Documentos de referencia