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 Navigation Compose te permite compilar con facilidad apps multipantalla en Compose a través de 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
- Crear un
NavHost
componible para definir rutas y pantallas en tu app - Navegar entre pantallas utilizando un
NavHostController
- Manipular la pila de actividades para navegar a pantallas anteriores
- Usar intents para compartir datos con otra app
- Personalizar 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 para descargar el código de partida
2. Descarga el código de partida
Para comenzar, descarga el código de partida:
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
Si deseas ver el código de partida para este codelab, míralo en GitHub.
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. Si ejecutas la app, no podrás ver nada ni podrás navegar entre estas pantallas, ya que el componente de navegación aún no está agregado al código de la app. No obstante, puedes verificar las previsualizaciones de los componibles de cada pantalla y compararlas con las pantallas finales de la aplicación que aparecen a continuación.
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 a través del elemento componible StartOrderScreen
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 a través del elemento componible SelectQuantityButton
, 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 cadenas 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 componible, 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 componible 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 a través del elemento componible OrderSummaryScreen
en SummaryScreen.kt
.
El diseño consiste en un Column
que contiene toda la información sobre el pedido, un elemento componible Text
para el subtotal y botones para 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 Android ShareSheet 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 componible CupcakeApp
. 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 componible NavHost 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 componibles reutilizables
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 componible FormattedPriceLabel
. 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 componible SelectOptionScreen
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 componible RadioButton
y un elemento Text
que contiene cada cadena. Una Column
rodea todo el diseño y también contiene un elemento Text
que admite composición para 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 componibles a los que se navegará.
- NavHost: Es el elemento componible 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 componible (o un grupo de ellos) que corresponde 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 elemento componibleCupcakeAppBar
, 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 componible 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 componible.
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 cadenas 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 componibles, NavHost
también toma un parámetro modifier
.
Agregarás un NavHost
al elemento componible CupcakeApp
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
. - Dentro de
Scaffold
, debajo de la variableuiState
, agrega un elementoNavHost
.
import androidx.navigation.compose.NavHost
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.
import androidx.compose.foundation.layout.padding
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
.
import androidx.navigation.compose.composable
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
. Para elmodifier
, pasaModifier.fillMaxSize().padding(dimensionResource(R.dimen.padding_medium))
.
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.res.dimensionResource
import com.example.cupcake.ui.StartOrderScreen
import com.example.cupcake.data.DataSource
NavHost(
navController = navController,
startDestination = CupcakeScreen.Start.name,
modifier = Modifier.padding(innerPadding)
) {
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
- 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
.Context
es una clase abstracta que se debe implementar por medio del sistema Android. Permite acceder a recursos y clases específicos de la aplicación, así como a llamadas ascendentes para operaciones a nivel de la aplicación, como el lanzamiento de actividades, etc. Puedes utilizar esta variable para obtener las cadenas de la lista de ID de recursos en el modelo de vista para mostrar la lista de sabores.
import androidx.compose.ui.platform.LocalContext
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
}
- Llama al elemento componible
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 cadenas de la app. Puedes transformar la lista de ID de recursos en una lista de cadenas usando la función
map()
y llamando acontext.resources.getString(id)
para cada sabor.
import com.example.cupcake.ui.SelectOptionScreen
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) }
)
}
- Para el parámetro
onSelectionChanged
, pasa una expresión lambda que llame asetFlavor()
en el modelo de vista y pasait
(el argumento que se pasa aonSelectionChanged()
). Para el parámetromodifier
, pasaModifier.fillMaxHeight().
.
import androidx.compose.foundation.layout.fillMaxHeight
import com.example.cupcake.data.DataSource.flavors
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
La pantalla de fecha de retiro es similar a la de sabores. La única diferencia son los datos que se pasan al elemento componible SelectOptionScreen
.
- 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
. Para el parámetromodifier
, pasaModifier.fillMaxHeight().
.
SelectOptionScreen(
subtotal = uiState.price,
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
- 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 componible
OrderSummaryScreen()
y pasa la variableuiState
para el parámetroorderUiState
. Para el parámetromodifier
, pasaModifier.fillMaxHeight().
.
import com.example.cupcake.ui.OrderSummaryScreen
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
modifier = Modifier.fillMaxHeight()
)
}
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 componibles 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 componibles.
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 componible 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 componible StartOrderScreen
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
){
...
}
- Ahora que el elemento componible
StartOrderScreen
espera un valor deonNextButtonClicked
, encuentra elStartOrderPreview
y pasa un cuerpo lambda vacío al parámetroonNextButtonClicked
.
@Preview
@Composable
fun StartOrderPreview() {
CupcakeTheme {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
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
para 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 a través de la primera o la segunda propiedad. En el caso del parámetro quantityOptions
del elemento componible StartOrderScreen
, el primer Int
es un ID de recurso para la cadena 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()
.
- Encuentra la expresión lambda vacía para el parámetro
onClick
deSelectQuantityButton
.
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 elemento componibleSelectOptionScreen
enSelectOptionScreen.kt
, agrega un parámetro llamadoonCancelButtonClicked
de tipo() -> Unit
con un valor predeterminado de{}
.
@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
con un valor predeterminado de{}
@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 componible
OrderSummaryScreen
, enSummaryScreen.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
(String, String) -> Unit
y asígnale el nombreonSendButtonClicked
.
@Composable
fun OrderSummaryScreen(
orderUiState: OrderUiState,
onCancelButtonClicked: () -> Unit,
onSendButtonClicked: (String, String) -> Unit,
modifier: Modifier = Modifier
){
...
}
- El elemento componible
OrderSummaryScreen
ahora espera valores paraonSendButtonClicked
yonCancelButtonClicked
. Encuentra elOrderSummaryPreview
, pasa un cuerpo lambda vacío con dos parámetrosString
aonSendButtonClicked
y un cuerpo lambda vacío a los parámetrosonCancelButtonClicked
.
@Preview
@Composable
fun OrderSummaryPreview() {
CupcakeTheme {
OrderSummaryScreen(
orderUiState = OrderUiState(0, "Test", "Test", "$300.00"),
onSendButtonClicked = { subject: String, summary: String -> },
onCancelButtonClicked = {},
modifier = Modifier.fillMaxHeight()
)
}
}
- Pasa
onSendButtonClicked
para el parámetroonClick
del botón Send. PasanewOrder
yorderSummary
, las dos variables definidas antes enOrderSummaryScreen
. Estas cadenas 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 = DataSource.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 = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
- 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 = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
- 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) },
modifier = Modifier.fillMaxHeight()
)
}
- 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) },
modifier = Modifier.fillMaxHeight()
)
- 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) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
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 cadena 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 para hacer esto y, luego, lo pasarás en 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 componible
CupcakeApp()
, pasacancelOrderAndNavigateToStart
para los parámetrosonCancelButtonClicked
de los dos elementosSelectOptionScreen
y el elementoOrderSummaryScreen
.
composable(route = CupcakeScreen.Start.name) {
StartOrderScreen(
quantityOptions = DataSource.quantityOptions,
onNextButtonClicked = {
viewModel.setQuantity(it)
navController.navigate(CupcakeScreen.Flavor.name)
},
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.padding_medium))
)
}
composable(route = CupcakeScreen.Flavor.name) {
val context = LocalContext.current
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = DataSource.flavors.map { id -> context.resources.getString(id) },
onSelectionChanged = { viewModel.setFlavor(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Pickup.name) {
SelectOptionScreen(
subtotal = uiState.price,
onNextButtonClicked = { navController.navigate(CupcakeScreen.Summary.name) },
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
options = uiState.pickupOptions,
onSelectionChanged = { viewModel.setDate(it) },
modifier = Modifier.fillMaxHeight()
)
}
composable(route = CupcakeScreen.Summary.name) {
OrderSummaryScreen(
orderUiState = uiState,
onCancelButtonClicked = {
cancelOrderAndNavigateToStart(viewModel, navController)
},
onSendButtonClicked = { subject: String, summary: String ->
},
modifier = Modifier.fillMaxHeight()
)
}
- 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 volver a la pantalla principal. Solo falta completar un paso más para 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 ShareSheet (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 componible
CupcakeApp
, crea una función privada llamadashareOrder()
.
private fun shareOrder()
- Agrega un parámetro llamado
context
de tipoContext
.
import android.content.Context
private fun shareOrder(context: Context) {
}
- Agrega dos parámetros de tipo
String
:subject
ysummary
. Estas cadenas 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.
import android.content.Intent
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 cadenasnew_cupcake_order
.
context.startActivity(
Intent.createChooser(
intent,
context.getString(R.string.new_cupcake_order)
)
)
- En el elemento componible
CupcakeApp
, en la llamada acomposable()
paraCucpakeScreen.Summary.name
, obtén una referencia al objeto de contexto para 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 app bar 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 componible llamado CupcakeAppBar
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 para mostrar el título correcto y el botón Up si corresponde. El elemento componible CupcakeAppBar
debe reconocer la pantalla actual de manera tal que el título se actualice correctamente.
- En la enum
CupcakeScreen
en CupcakeScreen.kt, agrega un parámetro de tipoInt
llamadotitle
usando la anotación@StringRes
.
import androidx.annotation.StringRes
enum class CupcakeScreen(@StringRes val title: Int) {
Start,
Flavor,
Pickup,
Summary
}
- Agrega un valor de recurso para cada caso de enum correspondiente al texto del título para cada pantalla. Usa
app_name
para la pantallaStart
,choose_flavor
para la pantallaFlavor
,choose_pickup_date
para la pantallaPickup
yorder_summary
para la pantallaSummary
.
enum class CupcakeScreen(@StringRes val title: Int) {
Start(title = R.string.app_name),
Flavor(title = R.string.choose_flavor),
Pickup(title = R.string.choose_pickup_date),
Summary(title = R.string.order_summary)
}
- Agrega un parámetro llamado
currentScreen
de tipoCupcakeScreen
al elemento componibleCupcakeAppBar
.
fun CupcakeAppBar(
currentScreen: CupcakeScreen,
canNavigateBack: Boolean,
navigateUp: () -> Unit = {},
modifier: Modifier = Modifier
)
- Dentro de
CupcakeAppBar
, reemplaza el nombre codificado de la app con el título de la pantalla actual pasandocurrentScreen.title
a la llamada parastringResource()
para el parámetro de título deTopAppBar
.
TopAppBar(
title = { Text(stringResource(currentScreen.title)) },
modifier = modifier,
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_button)
)
}
}
}
)
El botón Up solo debe mostrarse si hay un elemento componible en la pila de actividades. Si la app no tiene pantallas en la pila de actividades (es decir, si se muestra StartOrderScreen
), no debería mostrarse el botón Up. Para verificar esto, necesitas una referencia a la pila de actividades.
- En el elemento componible
CupcakeApp
, debajo de la variablenavController
, crea una variable llamadabackStackEntry
y llama al métodocurrentBackStackEntryAsState()
denavController
con el delegadoby
.
import androidx.navigation.compose.currentBackStackEntryAsState
@Composable
fun CupcakeApp(
viewModel: OrderViewModel = viewModel(),
navController: NavHostController = rememberNavController()
){
val backStackEntry by navController.currentBackStackEntryAsState()
...
}
- Convierte el título actual de la pantalla en un valor de
CupcakeScreen
. Debajo de la variablebackStackEntry
crea una variable utilizandoval
llamadacurrentScreen
igual al resultado de llamar la función de clasevalueOf()
deCupcakeScreen
, y pasa la ruta del destino debackStackEntry
. Usa el operador elvis para proporcionar un valor predeterminado deCupcakeScreen.Start.name
.
val currentScreen = CupcakeScreen.valueOf(
backStackEntry?.destination?.route ?: CupcakeScreen.Start.name
)
- Pasa el valor de la variable
currentScreen
al parámetro del mismo nombre del elemento componibleCupcakeAppBar
.
CupcakeAppBar(
currentScreen = currentScreen,
canNavigateBack = false,
navigateUp = {}
)
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 el botón Up 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 Up, 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 estos comandos 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 para 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.