Cómo navegar entre pantallas con Compose

1. Antes de comenzar

Hasta este momento, las apps en las que trabajaste tenían una sola pantalla. Sin embargo, es probable que muchas de las apps que uses tengan varias pantallas por las que puedas navegar. Por ejemplo, la app de Configuración tiene muchas páginas de contenido distribuidas en diferentes pantallas.

Primera página de la app de Configuración de Android.

Página de Configuración después de que el usuario selecciona "Dispositivos conectados" en la primera página.

Página de Configuración después de que el usuario selecciona "Vincular un dispositivo nuevo" en la página anterior.

En Modern Android Development, las apps multipantalla se crean con el componente Navigation de Jetpack. Este componente de Compose te permite compilar con facilidad apps multipantalla en Compose mediante un enfoque declarativo, tal como se compilan las interfaces de usuario. En este codelab, se presentan los aspectos básicos del componente Navigation de Compose, así como la forma de lograr que la AppBar sea responsiva y cómo enviar datos de tu app a otra con intents, además de demostrar las prácticas recomendadas en una app cada vez más compleja.

Requisitos previos

  • Conocimientos del lenguaje Kotlin, incluidos los tipos de funciones, las lambdas y las funciones de alcance
  • Conocimientos de diseños básicos de Row y Column en Compose

Qué aprenderás

  • Crea un elemento NavHost que admite composición para definir rutas y pantallas en tu app.
  • Navega entre pantallas con un NavHostController.
  • Manipula la pila de actividades a fin de navegar a pantallas anteriores.
  • Usa intents para compartir datos con otra app.
  • Personaliza la AppBar, incluidos el título y el botón Atrás.

Qué compilarás

  • Implementarás la navegación en una app multipantalla.

Requisitos

  • La versión más reciente de Android Studio
  • Conexión a Internet a fin de descargar el código de inicio

2. Descarga el código de inicio

Para comenzar, descarga el código de inicio:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout starter

3. Explicación de la app

La app de Cupcake es un poco diferente de las apps con las que trabajaste hasta ahora. En lugar de que todo el contenido se muestre en una sola pantalla, la app tiene cuatro pantallas distintas, y el usuario puede navegar por cada una de ellas mientras pide magdalenas.

Pantalla de inicio del pedido

La primera pantalla presenta al usuario tres botones que corresponden a la cantidad de magdalenas que pedirá.

La primera pantalla de la app de Cupcake muestra opciones para iniciar un pedido de una magdalena, seis o doce.

En el código, esto se representa mediante el elemento StartOrderScreen que admite composición en StartOrderScreen.kt.

La pantalla consta de una sola columna, con una imagen y texto, junto con tres botones personalizados para pedir diferentes cantidades de magdalenas. Los botones personalizados se implementan mediante el elemento SelectQuantityButton que admite composición, que también está en StartOrderScreen.kt.

Pantalla de selección de sabores

Después de seleccionar la cantidad, la app le pedirá al usuario que seleccione un sabor para la magdalena. La app usa lo que se conoce como botones de selección a fin de mostrar diferentes opciones. Los usuarios pueden seleccionar un sabor entre diversas opciones.

La app de Cupcake presenta al usuario opciones de diferentes sabores.

La lista de posibles sabores se almacena como una lista de IDs de recursos de strings en data.DataSource.kt.

Selecciona la pantalla de fecha de retiro

Después de elegir un sabor, la app presenta al usuario otra serie de botones de selección para que elija una fecha de retiro. Las opciones de retiro provienen de una lista que muestra la función pickupOptions() en OrderViewModel.

La app de Cupcake presenta al usuario opciones para la fecha de retiro.

Las pantallas Choose Flavor y Choose Pickup Date se representan con el mismo elemento que admite composición, SelectOptionScreen en SelectOptionScreen.kt. ¿Por qué usar el mismo elemento? Porque el diseño de estas pantallas es exactamente el mismo. La única diferencia son los datos, pero puedes usar el mismo elemento que admite composición para mostrar las pantallas de sabores y fecha de retiro.

Pantalla de resumen del pedido

Después de seleccionar la fecha de retiro, la app muestra la pantalla Order Summary, en la que el usuario puede revisar y completar el pedido.

La app de Cupcake presenta el resumen del pedido, incluida la cantidad, el sabor, la fecha de retiro y el subtotal, además de opciones para enviar el pedido a otra app o cancelarlo.

Esta pantalla se implementa mediante el elemento OrderSummaryScreen que admite composición en OrderSummaryScreen.kt.

El diseño consiste en un Column que contiene toda la información sobre el pedido, un elemento Text que admite composición para el subtotal y botones a fin de enviar el pedido a otra app o cancelarlo y volver a la primera pantalla.

Si los usuarios deciden enviar el pedido a otra app, la app de Cupcake muestra una hoja inferior con diferentes opciones para compartir.

La app de Cupcake presenta al usuario opciones para compartir, como SMS o correo electrónico.

El estado actual de la app se almacena en data.OrderUiState.kt. La clase de datos OrderUiState contiene propiedades para almacenar las selecciones que realiza el usuario en cada pantalla.

Las pantallas de la app se presentarán en el elemento CupcakeApp que admite composición. Sin embargo, en el proyecto inicial, la app simplemente muestra la primera pantalla. Por el momento, no es posible navegar por todas las pantallas de la app, pero no te preocupes, ya que para eso estás haciendo este codelab. Aprenderás a definir rutas de navegación, configurar un elemento NavHost que admite composición para navegar entre pantallas (también conocido como destino), realizar intents de integración con componentes de la IU del sistema (como la pantalla para compartir) y hacer que la AppBar responda a los cambios de navegación.

Elementos reutilizables que admiten composición

Las apps de ejemplo de este curso están diseñadas para implementar prácticas recomendadas cuando corresponda. La app de Cupcake no es la excepción. En el paquete ui.components, verás un archivo llamado CommonUi.kt que contiene un elemento FormattedPriceLabel que admite composición. Varias pantallas de la app usan este elemento para dar formato al precio del pedido de manera coherente. En lugar de duplicar el elemento Text con el mismo formato y los mismos modificadores, puedes definir FormattedPriceLabel una vez y volver a usarlo tantas veces como sea necesario para otras pantallas.

Las pantallas de sabores y fecha de retiro usan el elemento SelectOptionScreen que admite composición y que también se puede reutilizar. Este elemento toma un parámetro llamado options del tipo List<String> que representa las opciones que se mostrarán. Las opciones aparecen en una Row, que consta de un elemento RadioButton que admite composición y un elemento Text que contiene cada string. Una Column rodea todo el diseño y también contiene un elemento Text que admite composición a fin de mostrar el precio con formato, los botones Cancel y Next.

4. Cómo definir rutas y crear un NavHostController

Partes del componente Navigation

El componente Navigation tiene tres partes principales:

  • NavController: Es responsable de navegar entre los destinos, es decir, las pantallas en tu app.
  • NavGraph: Realiza la asignación de los destinos que admiten composición a los que se navegará.
  • NavHost: Es el elemento que admite composición y que funciona como contenedor para mostrar el destino actual del NavGraph.

En este codelab, te enfocarás en el NavController y el NavHost. Dentro del NavHost, definirás los destinos para el NavGraph de la app de Cupcake.

Cómo definir las rutas para los destinos en tu app

Uno de los conceptos fundamentales de la navegación en una app de Compose es la ruta. Una ruta es una string que se corresponde con un destino. Esta idea es similar al concepto de una URL. Así como una URL diferente se asigna a una página diferente en un sitio web, una ruta es una string que se asigna a un destino y sirve como su identificador único. Por lo general, un destino es un único elemento que admite composición (o un grupo de ellos) que corresponden a lo que ve el usuario. La app de Cupcake necesita destinos para la pantalla de inicio del pedido, la pantalla de sabores, la pantalla de fecha de retiro y la pantalla de resumen del pedido.

Hay una cantidad limitada de pantallas en una app, por lo que también hay una cantidad limitada de rutas. Puedes definir las rutas de una app mediante una clase de tipo enum. En Kotlin, estas clases tienen una propiedad de nombre que muestra una string con el nombre de la propiedad.

Comenzarás por definir las cuatro rutas de la app de Cupcake.

  • Start: Selecciona la cantidad de magdalenas optando por uno de los tres botones.
  • Flavor: Selecciona el sabor a partir de una lista de opciones.
  • Pickup: Selecciona la fecha de retiro a partir de una lista de opciones.
  • Summary: Revisa las selecciones y envía o cancela el pedido.

Agrega una clase de tipo enum para definir las rutas.

  1. En CupcakeScreen.kt, encima del elemento CupcakeAppBar que admite composición, agrega una clase de tipo enum llamada CupcakeScreen.
enum class CupcakeScreen() {

}
  1. Agrega cuatro casos a la clase enum: Start, Flavor, Pickup y Summary.
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

Cómo agregar un NavHost a tu app

Un NavHost es un elemento que admite composición y que muestra otros destinos, según una ruta determinada. Por ejemplo, si la ruta es Flavor, NavHost mostrará la pantalla para elegir el sabor de la magdalena. Si la ruta es Summary, la app mostrará la pantalla de resumen.

La sintaxis de NavHost es como cualquier otro elemento que admite composición.

fae7688d6dd53de9.png

Se destacan dos parámetros.

  • navController: Es una instancia de la clase NavHostController. Puedes usar este objeto a fin de navegar entre pantallas, por ejemplo, si llamas al método navigate() para navegar a otro destino. Puedes obtener el NavHostController si llamas a rememberNavController() desde una función de componibilidad.
  • startDestination: Es una ruta de strings que define el destino que se muestra de forma predeterminada cuando la app muestra el NavHost por primera vez. En el caso de la app de Cupcake, esta debería ser la ruta Start.

Al igual que otros elementos que admiten composición, NavHost también toma un parámetro modifier.

Agregarás un NavHost al elemento CupcakeApp que admite composición en CupcakeScreen.kt. Primero, necesitas una referencia al controlador de navegación. Puedes usar este controlador tanto en el NavHost que estás agregando ahora como en el AppBar que agregarás en un paso posterior. Por lo tanto, debes declarar la variable en el elemento CupcakeApp().

  1. Abre CupcakeScreen.kt.
  2. Arriba de la variable viewModel en el elemento CupcakeApp, crea una variable nueva con un elemento val llamado navController y establécelo en el resultado de la llamada a rememberNavController().
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...
}
  1. Dentro de Scaffold, debajo de la variable uiState, agrega un elemento NavHost.
Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. Pasa la variable navController para el parámetro navController y CupcakeScreen.Start.name para el parámetro startDestination. Pasa el modificador que se pasó a CupcakeApp() para el parámetro del modificador. Pasa una lambda final vacía para el parámetro final.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
}

Cómo administrar las rutas en tu NavHost

Al igual que otros elementos que admiten composición, NavHost toma un tipo de función para su contenido.

f67974b7fb3f0377.png

Dentro de la función de contenido de un NavHost, debes llamar a la función composable(). La función composable() tiene dos parámetros obligatorios.

  • route: Es una string que corresponde al nombre de una ruta. Puede ser cualquier string única. Usarás la propiedad de nombre de las constantes de la clase enum CupcakeScreen.
  • content: Aquí puedes llamar a un elemento que deseas mostrar para la ruta determinada.

Llamarás a la función composable() una vez para cada una de las cuatro rutas.

  1. Llama a la función composable() y pasa CupcakeScreen.Start.name para la route.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Dentro de la expresión lambda final, llama al elemento StartOrderScreen y pasa quantityOptions para la propiedad quantityOptions.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = quantityOptions
        )
    }
}
  1. Debajo de la primera llamada a composable(), vuelve a llamar a composable() y pasa CupcakeScreen.Flavor.name para la route.
composable(route = CupcakeScreen.Flavor.name) {

}
  1. Dentro de la expresión lambda final, obtén una referencia a LocalContext.current y almacénala en una variable llamada context. Puedes usar esta variable con el fin de obtener las strings de la lista de IDs de recursos en el modelo de vistas a los efectos de mostrar la lista de sabores.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current

}
  1. Llama al elemento SelectOptionScreen.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. La pantalla de sabores debe mostrar y actualizar el subtotal cuando el usuario selecciona un sabor. Pasa uiState.price para el parámetro subtotal.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. La pantalla de sabores obtiene la lista de opciones de los recursos de strings de la app. Crea una lista de strings a partir de la lista de sabores del modelo de vistas. Puedes transformar la lista de IDs de recursos en una lista de strings usando la función map() y llamando a stringResource().
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> stringResource(id) }
    )
}
  1. Para el parámetro onSelectionChanged, pasa una expresión lambda que llame a setFlavor() en el modelo de vistas y pasa it (el argumento que se pasa a onSelectionChanged()).
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}

La pantalla de fecha de retiro es similar a la de sabores. La única diferencia son los datos que se pasan al elemento SelectOptionScreen que admite composición.

  1. Vuelve a llamar a la función composable() y pasa CupcakeScreen.Pickup.name para el parámetro route.
composable(route = CupcakeScreen.Pickup.name) {

}
  1. En la expresión lambda final, llama al elemento SelectOptionScreen y pasa uiState.price para el subtotal, como antes. Pasa uiState.pickupOptions para el parámetro options y una expresión lambda que llame a setDate() en el viewModel para el parámetro onSelectionChanged.
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Llama a composable() una vez más y pasa CupcakeScreen.Summary.name para la route.
composable(route = CupcakeScreen.Summary.name) {

}
  1. Dentro de la expresión lambda final, llama al elemento OrderSummaryScreen() que admite composición y pasa la variable uiState para el parámetro orderUiState.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState
    )
}

Eso es todo lo que se necesita para configurar el NavHost. En la siguiente sección, harás que tu app cambie de ruta y navegue entre pantallas cuando el usuario presione cada uno de los botones.

5. Cómo navegar entre rutas

Ahora que definiste las rutas y las asignaste a elementos que admiten composición en un NavHost, es hora de navegar entre pantallas. El NavHostController (la propiedad del navController que surge de llamar a rememberNavController()) es el responsable de navegar entre rutas. Sin embargo, ten en cuenta que esta propiedad se define en el elemento CupcakeApp. Necesitas una forma de acceder a él desde las diferentes pantallas de tu app.

Fácil, ¿verdad? Solo pasa navController como parámetro a cada uno de los elementos que admiten composición.

Si bien este enfoque funciona, no es una forma ideal de diseñar tu app. Un beneficio de usar un NavHost para manejar la navegación de tu app es que la lógica de navegación se mantiene independiente de la IU individual. Esta opción evita algunas de las principales desventajas de pasar navController como parámetro.

  • La lógica de navegación se guarda en un solo lugar, lo que puede facilitar el mantenimiento de tu código y evitar errores, ya que no da vía libre de forma accidental a las pantallas individuales para la navegación en tu app.
  • En las apps que necesitan trabajar con diferentes factores de forma (como un teléfono en modo Retrato, un teléfono plegable o una tablet con pantalla grande), es posible que un botón active la navegación, según el diseño de la app. Las pantallas individuales deben ser independientes, y no es necesario que tengan en cuenta otras pantallas de la app.

En cambio, nuestro enfoque consiste en pasar un tipo de función a cada elemento que admite composición para lo que debe suceder cuando un usuario hace clic en el botón. De esa manera, el elemento y cualquiera de sus elementos secundarios deciden cuándo llamar a la función. Sin embargo, la lógica de navegación no está expuesta a las pantallas individuales de tu app. Todo el comportamiento de navegación se controla en el NavHost.

Cómo agregar controladores de botones a StartOrderScreen

Comenzarás por agregar un parámetro de tipo de función al que se llama cuando se presiona uno de los botones de cantidad en la primera pantalla. Esta función se pasa al elemento StartOrderScreen que admite composición y es responsable de actualizar el viewmodel y navegar a la siguiente pantalla.

  1. Abre StartOrderScreen.kt.
  2. Debajo del parámetro quantityOptions y antes del parámetro modificador, agrega un parámetro llamado onNextButtonClicked de tipo () -> Unit.
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
...
}

Cada botón corresponde a una cantidad diferente de magdalenas. Necesitarás esta información de modo que la función pasada de onNextButtonClicked pueda actualizar el viewmodel según corresponda.

  1. Modifica el tipo del parámetro onNextButtonClicked a fin de que tome un parámetro Int.
onNextButtonClicked: (Int) -> Unit,

Para obtener el Int que se pasará cuando se llame a onNextButtonClicked(), observa el tipo de parámetro quantityOptions.

El tipo es List<Pair<Int, Int>> o una lista de Pair<Int, Int>. Es posible que no conozcas el tipo Pair, pero, tal como sugiere su nombre, consiste en un par de valores. Pair toma dos parámetros de tipo genérico. En este caso, ambos son del tipo Int.

8326701a77706258.png

Se puede acceder a cada elemento de un par mediante la primera o la segunda propiedad. En el caso del parámetro quantityOptions del elemento StartOrderScreen que admite composición, el primer Int es un IDs de recurso para la string que se mostrará en cada botón. El segundo Int es la cantidad real de magdalenas.

Pasaremos la segunda propiedad del par seleccionado cuando llames a la función onNextButtonClicked().

  1. Pasa una expresión lambda para el parámetro onClick del botón SelectQuantityButton.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {  }
    )
}
  1. Dentro de la expresión lambda, llama a onNextButtonClicked y pasa item.second, la cantidad de magdalenas.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

Cómo agregar controladores de botones a SelectOptionScreen

  1. Debajo del parámetro onSelectionChanged del elemento SelectOptionScreen que admite composición en SelectOptionScreen.kt, agrega un parámetro llamado onCancelButtonClicked de tipo () -> Unit.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Debajo del parámetro onCancelButtonClicked, agrega otro parámetro de tipo () -> Unit llamado onNextButtonClicked.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Pasa onCancelButtonClicked para el parámetro onClick del botón Cancel.
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
    Text(stringResource(R.string.cancel))
}
  1. Pasa onNextButtonClicked para el parámetro onClick del botón Next.
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

Cómo agregar controladores de botones a SummaryScreen

Por último, agrega las funciones del controlador de botones para los botones Cancel y Send en la pantalla de resumen.

  1. En el elemento OrderSummaryScreen que admite composición, en OrderSummaryScreen.kt, agrega un parámetro llamado onCancelButtonClicked de tipo () -> Unit.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Agrega otro parámetro de tipo () -> Unit y asígnale el nombre onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Pasa onSendButtonClicked para el parámetro onClick del botón Send. Pasa newOrder y orderSummary, las dos variables definidas antes en OrderSummaryScreen. Estas strings consisten en los datos reales que el usuario puede compartir con otra app.
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. Pasa onCancelButtonClicked para el parámetro onClick del botón Cancel.
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

Para navegar a otra ruta, simplemente llama al método navigate() en tu instancia de NavHostController.

fc8aae3911a6a25d.png

El método de navegación toma un solo parámetro: una string que corresponde a una ruta definida en tu NavHost. Si la ruta coincide con una de las llamadas a composable() en el NavHost, la app navega a esa pantalla.

Pasarás funciones que llamen a navigate() cuando el usuario presione botones en las pantallas Start, Flavor y Pickup.

  1. En CupcakeScreen.kt, busca la llamada a composable() para la pantalla de inicio. Para el parámetro onNextButtonClicked, pasa una expresión lambda.
StartOrderScreen(
    quantityOptions = quantityOptions,
    onNextButtonClicked = {
    }
)

¿Recuerdas la propiedad Int que se pasó a esta función para la cantidad de magdalenas? Antes de navegar a la pantalla siguiente, debes actualizar el modelo de vistas de modo que la app muestre el subtotal correcto.

  1. Llama a setQuantity en el viewModel y pasa it.
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. Llama a navigate() en el navController y pasa CupcakeScreen.Flavor.name para el route.
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. Para el parámetro onNextButtonClicked en la pantalla de sabores, simplemente pasa una lambda que llame a navigate() y pasa CupcakeScreen.Pickup.name para la route.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Pickup.name) },
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}
  1. Pasa una lambda vacía para onCancelButtonClicked, que implementarás a continuación.
SelectOptionScreen(
     subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) }
)
  1. Para el parámetro onNextButtonClicked en la pantalla de retiro, pasa una lambda que llame a navigate() y pasa CupcakeScreen.Summary.name para la route.
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Summary.name)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) }
    )
}
  1. Una vez más, pasa una lambda vacía para onCancelButtonClicked().
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Para OrderSummaryScreen, pasa lambdas vacías para onCancelButtonClicked y onSendButtonClicked. Agrega los parámetros para el subject y el summary que se pasan a onSendButtonClicked, que implementarás pronto.
composable(route = CupcakeScreen.Summary.name) {
   val context = LocalContext.current
   OrderSummaryScreen(
       orderUiState = uiState,
       onCancelButtonClicked = {},
       onSendButtonClicked = { subject: String, summary: String ->

       }
   )
}

Ahora deberías poder navegar por cada pantalla de tu app. Ten en cuenta que, si llamas a navigate(), no solo cambiará la pantalla, sino que se colocará encima de la pila de actividades. Además, cuando presionas el botón del sistema para ir hacia atrás, podrás volver a la pantalla anterior.

La app apila cada pantalla en la parte superior de la anterior, y el botón para ir hacia atrás ( bade5f3ecb71e4a2.png) puede quitarlas. El historial de pantallas desde el elemento startDestination en la parte inferior hasta la parte superior que se acaba de mostrar se conoce como la pila de actividades.

Cómo ir a la pantalla de inicio

A diferencia del botón para ir hacia atrás del sistema, el botón Cancel no vuelve a la pantalla anterior. En cambio, debe quitar todas las pantallas de la pila de actividades y volver a la pantalla de inicio.

Puedes hacer esto llamando al método popBackStack().

2f382e5eb319b4b8.png

El método popBackStack() tiene dos parámetros obligatorios.

  • route: Es la string que representa la ruta del destino al que deseas volver.
  • inclusive: Es un valor booleano que, si es verdadero, también muestra (quita) la ruta especificada. Si es falso, popBackStack() quitará todos los destinos que se encuentren sobre el de inicio (pero no este último), lo que hará que sea la pantalla superior visible para el usuario.

Cuando los usuarios presionan el botón Cancel en cualquiera de las pantallas, la app restablece el estado del modelo de vistas y llama a popBackStack(). Primero, implementarás un método a fin de hacer esto y, luego, lo pasarás para el parámetro adecuado en las tres pantallas con los botones Cancel.

  1. Después de la función CupcakeApp(), define una función privada llamada cancelOrderAndNavigateToStart().
private fun cancelOrderAndNavigateToStart() {
}
  1. Agrega dos parámetros: viewModel de tipo OrderViewModel y navController de tipo NavHostController.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. En el cuerpo de la función, llama a resetOrder() en el viewModel.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. Llama a popBackStack() en el navController, pasa CupcakeScreen.Start.name para la route y false para el parámetro inclusive.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. En el elemento CupcakeApp() que admite composición, pasa cancelOrderAndNavigateToStart para los parámetros onCancelButtonClicked de los dos elementos SelectOptionScreen y el elemento OrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
   StartOrderScreen(
       quantityOptions = quantityOptions,
       onNextButtonClicked = {
           viewModel.setQuantity(it)
           navController.navigate(CupcakeScreen.Flavor.name)
       }
   )
}
composable(route = CupcakeScreen.Flavor.name) {
   val context = LocalContext.current
   SelectOptionScreen(
       subtotal = uiState.price,
       onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
       onCancelButtonClicked = {
           cancelOrderAndNavigateToStart(viewModel, navController)
       },
       options = flavors.map { id -> context.resources.getString(id) },
       onSelectionChanged = { viewModel.setFlavor(it) }
   )
}
  1. Ejecuta tu app y prueba si, cuando se presiona el botón Cancel en cualquiera de las pantallas, el usuario vuelve a la primera pantalla.

6. Cómo navegar a otra app

Hasta ahora, aprendiste a navegar a una pantalla diferente en tu app y a la pantalla raíz. Solo falta completar un paso más a fin de implementar la navegación en la app de Cupcake. En la pantalla de resumen del pedido, el usuario puede enviar su pedido a otra app. Esta selección abre una hoja inferior (un componente de la interfaz de usuario que cubre la parte inferior de la pantalla) que muestra las opciones para compartir.

Esta parte de la IU no forma parte de la app de Cupcake. De hecho, lo proporciona el sistema operativo Android. La IU del sistema, como la pantalla para compartir, no recibe llamadas de tu navController. En su lugar, usarás algo llamado Intent.

Un intent es una solicitud para que el sistema realice alguna acción, en general, presentando una actividad nueva. Existen muchos intents diferentes, y te recomendamos que consultes la documentación con el fin de obtener una lista completa. Sin embargo, nos interesa el que se llama ACTION_SEND. Puedes enviarle algunos datos a este intent, como una string, y presentar las acciones de uso compartido adecuadas para esos datos.

El proceso básico para configurar un intent es el siguiente:

  1. Crea un objeto de intent y especifica el intent, como ACTION_SEND.
  2. Especifica el tipo de datos adicionales que se envían con el intent. Para un texto simple, puedes usar "text/plain", aunque hay otros tipos disponibles, como "image/*" o "video/*".
  3. Pasa cualquier dato adicional al intent, como el texto o la imagen que se compartirá, llamando al método putExtra(). Este intent tendrá dos extras: EXTRA_SUBJECT y EXTRA_TEXT.
  4. Llama al método startActivity() de contexto y pasa una actividad creada a partir del intent.

Te explicaremos cómo crear el intent de acción de uso compartido, pero el proceso es el mismo para otros tipos de intents. En proyectos futuros, te recomendamos que consultes la documentación según sea necesario para el tipo específico de datos y los extras necesarios.

Completa los siguientes pasos con el fin de crear un intent de modo que se envíe el pedido de magdalenas a otra app:

  1. En CupcakeScreen.kt, debajo del elemento CupcakeApp que admite composición, crea una función privada llamada shareOrder().
private fun shareOrder()
  1. Agrega un parámetro llamado context de tipo Context.
private fun shareOrder(context: Context) {
}
  1. Agrega dos parámetros de tipo String: subject y summary. Estas strings se mostrarán en la hoja de acciones para compartir.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. Dentro del cuerpo de la función, crea un intent llamado intent y pasa Intent.ACTION_SEND como argumento.
val intent = Intent(Intent.ACTION_SEND)

Dado que solo necesitas configurar este objeto Intent una vez, puedes hacer que las siguientes líneas de código resulten más concisas mediante la función apply(), que aprendiste en un codelab anterior.

  1. Llama a apply() en el intent recién creado y pasa una expresión lambda.
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. En el cuerpo de la lambda, establece el tipo en "text/plain". Debido a que estás haciendo esto en una función pasada a apply(), no necesitas hacer referencia al identificador del objeto, intent.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. Llama a putExtra() y pasa el asunto de EXTRA_SUBJECT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. Llama a putExtra() y pasa el resumen de EXTRA_TEXT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. Llama al método startActivity() de contexto.
context.startActivity(

)
  1. Dentro de la expresión lambda pasada a startActivity(), crea una actividad desde el intent llamando al método de clase createChooser(). Pasa el intent del primer argumento y el recurso de strings new_cupcake_order.
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. En el elemento CupcakeApp que admite composición, en la llamada a composable() para CucpakeScreen.Summary.name, obtén una referencia al objeto de contexto a fin de que puedas pasarlo a la función shareOrder().
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. En el cuerpo de la lambda de onSendButtonClicked(), llama a shareOrder() y pasa context, subject y summary como argumentos.
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. Ejecuta tu app y navega por las pantallas.

Cuando hagas clic en Send Order to Another App, deberías ver las acciones de uso compartido, como Messaging y Bluetooth, en la hoja inferior, junto con el asunto y el resumen que proporcionaste como extras.

La app de Cupcake presenta al usuario opciones para compartir, como SMS o correo electrónico.

7. Cómo hacer que la AppBar responda a la navegación

Si bien tu app funciona y puede navegar desde cada pantalla y hacia ellas, aún falta algo en las capturas de pantalla que vimos al comienzo de este codelab. La AppBar no responde automáticamente a la navegación. El título no se actualiza cuando la app navega a una ruta nueva ni muestra el botón Up antes del título cuando corresponde.

El código de inicio incluye un elemento llamado CupcakeAppBar que admite composición y sirve para administrar la AppBar. Ahora que implementaste la navegación en la app, puedes usar la información de la pila de actividades a fin de mostrar el título correcto y el botón Up si corresponde.

El botón Up solo debe mostrarse si hay un elemento que admite composición en la pila de actividades. Si la app no tiene pantallas en la pila de actividades (es decir, si se muestra la pantalla StartOrderScreen), no debería mostrarse el botón Up. Para verificar esto, necesitas una referencia a la pila de actividades.

  1. En el elemento CupcakeApp que admite composición, debajo de la variable navController, crea una variable llamada backStackEntry y llama al método currentBackStackEntry() de navController con el delegado by.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){

    val navController = rememberNavController()

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. En la CupcakeAppBar, pasa backStackEntry?.destination?.route para el parámetro currentScreen. Debido a que este valor admite un valor nulo, usa el operador elvis (?:) a fin de especificar CupcakeScreen.Start.name como valor el predeterminado.
currentScreen = backStackEntry?.destination?.route ?: CupcakeScreen.Start.name,

Siempre que en la pila de actividades haya una pantalla detrás de la actual, debería aparecer el botón Up. Puedes usar una expresión booleana para identificar si este botón debe aparecer.

  1. Para el parámetro canNavigateBack, pasa una expresión booleana que verifique si la propiedad previousBackStackEntry de navController es distinta del valor nulo.
canNavigateBack = navController.previousBackStackEntry != null,
  1. A fin de volver a la pantalla anterior, llama al método navigateUp() de navController.
navigateUp = { navController.navigateUp() }
  1. Ejecuta tu app.

Verás que el título AppBar ahora se actualiza y refleja la pantalla actual. Cuando navegues a una pantalla que no sea StartOrderScreen, debería aparecer el botón Arriba, que te llevará a la pantalla anterior.

La animación muestra la navegación del usuario por todas las pantallas de la app de Cupcake completa.

8. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar este comando de git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout navigation

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución para este codelab, míralo en GitHub.

9. Resumen

¡Felicitaciones! Acabas de pasar de trabajar en aplicaciones simples de una pantalla a una app compleja y multipantalla con el componente Navigation de Jetpack para desplazarte por varias pantallas. Definiste rutas, las controlaste en un NavHost y usaste parámetros de tipo de función a fin de separar la lógica de navegación de las pantallas individuales. También aprendiste a enviar datos a otra app mediante intents y a personalizar la barra de la aplicación en respuesta a la navegación. En las próximas unidades, seguirás usando estas habilidades mientras trabajas en varias apps multipantalla de mayor complejidad.

Más información