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.
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
yColumn
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:
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á.
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 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
.
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.
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.
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.
- En
CupcakeScreen.kt
, encima del elementoCupcakeAppBar
que admite composición, agrega una clase de tipo enum llamadaCupcakeScreen
.
enum class CupcakeScreen() {
}
- Agrega cuatro casos a la clase enum:
Start
,Flavor
,Pickup
ySummary
.
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.
Se destacan dos parámetros.
navController
: Es una instancia de la claseNavHostController
. Puedes usar este objeto a fin de navegar entre pantallas, por ejemplo, si llamas al métodonavigate()
para navegar a otro destino. Puedes obtener elNavHostController
si llamas arememberNavController()
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 elNavHost
por primera vez. En el caso de la app de Cupcake, esta debería ser la rutaStart
.
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()
.
- Abre
CupcakeScreen.kt
. - Arriba de la variable
viewModel
en el elementoCupcakeApp
, crea una variable nueva con un elementoval
llamadonavController
y establécelo en el resultado de la llamada arememberNavController()
.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
val navController = rememberNavController()
...
}
- Dentro de
Scaffold
, debajo de la variableuiState
, agrega un elementoNavHost
.
Scaffold(
...
) { innerPadding ->
val uiState by viewModel.uiState.collectAsState()
NavHost()
}
- Pasa la variable
navController
para el parámetronavController
yCupcakeScreen.Start.name
para el parámetrostartDestination
. Pasa el modificador que se pasó aCupcakeApp()
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.
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 enumCupcakeScreen
.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.
- Llama a la función
composable()
y pasaCupcakeScreen.Start.name
para laroute
.
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
}
}
- Dentro de la expresión lambda final, llama al elemento
StartOrderScreen
y pasaquantityOptions
para la propiedadquantityOptions
.
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = quantityOptions
)
}
}
- Debajo de la primera llamada a
composable()
, vuelve a llamar acomposable()
y pasaCupcakeScreen.Flavor.name
para laroute
.
composable(route = CupcakeScreen.Flavor.name) {
}
- Dentro de la expresión lambda final, obtén una referencia a
LocalContext.current
y almacénala en una variable llamadacontext
. 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
}
- Llama al elemento
SelectOptionScreen
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
)
}
- La pantalla de sabores debe mostrar y actualizar el subtotal cuando el usuario selecciona un sabor. Pasa
uiState.price
para el parámetrosubtotal
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price
)
}
- 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 astringResource()
.
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = flavors.map { id -> stringResource(id) }
)
}
- Para el parámetro
onSelectionChanged
, pasa una expresión lambda que llame asetFlavor()
en el modelo de vistas y pasait
(el argumento que se pasa aonSelectionChanged()
).
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.
- Vuelve a llamar a la función
composable()
y pasaCupcakeScreen.Pickup.name
para el parámetroroute
.
composable(route = CupcakeScreen.Pickup.name) {
}
- En la expresión lambda final, llama al elemento
SelectOptionScreen
y pasauiState.price
para elsubtotal
, como antes. PasauiState.pickupOptions
para el parámetrooptions
y una expresión lambda que llame asetDate()
en elviewModel
para el parámetroonSelectionChanged
.
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) }
)
- Llama a
composable()
una vez más y pasaCupcakeScreen.Summary.name
para laroute
.
composable(route = CupcakeScreen.Summary.name) {
}
- Dentro de la expresión lambda final, llama al elemento
OrderSummaryScreen()
que admite composición y pasa la variableuiState
para el parámetroorderUiState
.
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.
- Abre
StartOrderScreen.kt
. - Debajo del parámetro
quantityOptions
y antes del parámetro modificador, agrega un parámetro llamadoonNextButtonClicked
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.
- Modifica el tipo del parámetro
onNextButtonClicked
a fin de que tome un parámetroInt
.
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
.
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()
.
- Pasa una expresión lambda para el parámetro
onClick
del botónSelectQuantityButton
.
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { }
)
}
- Dentro de la expresión lambda, llama a
onNextButtonClicked
y pasaitem.second
, la cantidad de magdalenas.
quantityOptions.forEach { item ->
SelectQuantityButton(
labelResourceId = item.first,
onClick = { onNextButtonClicked(item.second) }
)
}
Cómo agregar controladores de botones a SelectOptionScreen
- Debajo del parámetro
onSelectionChanged
del elementoSelectOptionScreen
que admite composición enSelectOptionScreen.kt
, agrega un parámetro llamadoonCancelButtonClicked
de tipo() -> Unit
.
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- Debajo del parámetro
onCancelButtonClicked
, agrega otro parámetro de tipo() -> Unit
llamadoonNextButtonClicked
.
@Composable
fun SelectOptionScreen(
subtotal: String,
options: List<String>,
onSelectionChanged: (String) -> Unit = {},
onCancelButtonClicked: () -> Unit = {},
onNextButtonClicked: () -> Unit = {},
modifier: Modifier = Modifier
)
- Pasa
onCancelButtonClicked
para el parámetroonClick
del botón Cancel.
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
Text(stringResource(R.string.cancel))
}
- Pasa
onNextButtonClicked
para el parámetroonClick
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.
- En el elemento
OrderSummaryScreen
que admite composición, enOrderSummaryScreen.kt
, agrega un parámetro llamadoonCancelButtonClicked
de tipo() -> Unit
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
modifier: Modifier = Modifier
){
...
}
- Agrega otro parámetro de tipo
() -> Unit
y asígnale el nombreonSendButtonClicked
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- Pasa
onSendButtonClicked
para el parámetroonClick
del botón Send. PasanewOrder
yorderSummary
, las dos variables definidas antes enOrderSummaryScreen
. 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))
}
- Pasa
onCancelButtonClicked
para el parámetroonClick
del botón Cancel.
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onCancelButtonClicked
) {
Text(stringResource(R.string.cancel))
}
Cómo navegar a otra ruta
Para navegar a otra ruta, simplemente llama al método navigate()
en tu instancia de NavHostController
.
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
.
- En
CupcakeScreen.kt
, busca la llamada acomposable()
para la pantalla de inicio. Para el parámetroonNextButtonClicked
, 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.
- Llama a
setQuantity
en elviewModel
y pasait
.
onNextButtonClicked = {
viewModel.setQuantity(it)
}
- Llama a
navigate()
en elnavController
y pasaCupcakeScreen.Flavor.name
para elroute
.
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
}
- Para el parámetro
onNextButtonClicked
en la pantalla de sabores, simplemente pasa una lambda que llame anavigate()
y pasaCupcakeScreen.Pickup.name
para laroute
.
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) }
)
}
- 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) }
)
- Para el parámetro
onNextButtonClicked
en la pantalla de retiro, pasa una lambda que llame anavigate()
y pasaCupcakeScreen.Summary.name
para laroute
.
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = {
navController.navigate(CupcakeScreen.Summary.name)
},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) }
)
}
- 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) }
)
- Para
OrderSummaryScreen
, pasa lambdas vacías paraonCancelButtonClicked
yonSendButtonClicked
. Agrega los parámetros para elsubject
y elsummary
que se pasan aonSendButtonClicked
, 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 ( ) 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()
.
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.
- Después de la función
CupcakeApp()
, define una función privada llamadacancelOrderAndNavigateToStart()
.
private fun cancelOrderAndNavigateToStart() {
}
- Agrega dos parámetros:
viewModel
de tipoOrderViewModel
ynavController
de tipoNavHostController
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
}
- En el cuerpo de la función, llama a
resetOrder()
en elviewModel
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
}
- Llama a
popBackStack()
en elnavController
, pasaCupcakeScreen.Start.name
para laroute
yfalse
para el parámetroinclusive
.
private fun cancelOrderAndNavigateToStart(
viewModel: OrderViewModel,
navController: NavHostController
) {
viewModel.resetOrder()
navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
- En el elemento
CupcakeApp()
que admite composición, pasacancelOrderAndNavigateToStart
para los parámetrosonCancelButtonClicked
de los dos elementosSelectOptionScreen
y el elementoOrderSummaryScreen
.
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) }
)
}
- 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:
- Crea un objeto de intent y especifica el intent, como
ACTION_SEND
. - 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/*"
. - 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
yEXTRA_TEXT
. - 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:
- En CupcakeScreen.kt, debajo del elemento
CupcakeApp
que admite composición, crea una función privada llamadashareOrder()
.
private fun shareOrder()
- Agrega un parámetro llamado
context
de tipoContext
.
private fun shareOrder(context: Context) {
}
- Agrega dos parámetros de tipo
String
:subject
ysummary
. Estas strings se mostrarán en la hoja de acciones para compartir.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
- Dentro del cuerpo de la función, crea un intent llamado
intent
y pasaIntent.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.
- Llama a
apply()
en el intent recién creado y pasa una expresión lambda.
val intent = Intent(Intent.ACTION_SEND).apply {
}
- En el cuerpo de la lambda, establece el tipo en
"text/plain"
. Debido a que estás haciendo esto en una función pasada aapply()
, no necesitas hacer referencia al identificador del objeto,intent
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
}
- Llama a
putExtra()
y pasa el asunto deEXTRA_SUBJECT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
}
- Llama a
putExtra()
y pasa el resumen deEXTRA_TEXT
.
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, summary)
}
- Llama al método
startActivity()
de contexto.
context.startActivity(
)
- Dentro de la expresión lambda pasada a
startActivity()
, crea una actividad desde el intent llamando al método de clasecreateChooser()
. Pasa el intent del primer argumento y el recurso de stringsnew_cupcake_order
.
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- En el elemento
CupcakeApp
que admite composición, en la llamada acomposable()
paraCucpakeScreen.Summary.name
, obtén una referencia al objeto de contexto a fin de que puedas pasarlo a la funciónshareOrder()
.
composable(route = CupcakeScreen.Summary.name) {
val context = LocalContext.current
...
}
- En el cuerpo de la lambda de
onSendButtonClicked()
, llama ashareOrder()
y pasacontext
,subject
ysummary
como argumentos.
onSendButtonClicked = { subject: String, summary: String ->
shareOrder(context, subject = subject, summary = summary)
}
- 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.
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.
- En el elemento
CupcakeApp
que admite composición, debajo de la variablenavController
, crea una variable llamadabackStackEntry
y llama al métodocurrentBackStackEntry()
denavController
con el delegadoby
.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- En la
CupcakeAppBar
, pasabackStackEntry?.destination?.route
para el parámetrocurrentScreen
. Debido a que este valor admite un valor nulo, usa el operador elvis (?:
) a fin de especificarCupcakeScreen.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.
- Para el parámetro
canNavigateBack
, pasa una expresión booleana que verifique si la propiedadpreviousBackStackEntry
denavController
es distinta del valor nulo.
canNavigateBack = navController.previousBackStackEntry != null,
- A fin de volver a la pantalla anterior, llama al método
navigateUp()
denavController
.
navigateUp = { navController.navigateUp() }
- 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.
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.
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.