1. Antes de comenzar
En este codelab, terminarás de implementar el resto de la app de Cupcake, que comenzaste en un codelab anterior. La app de Cupcake tiene varias pantallas y muestra un flujo de pedidos para los cupcakes. La app completada debe permitir que el usuario navegue por ella para lo siguiente:
- Crear un pedido de cupcake
- Usar los botones Up o Back para ir a un paso anterior del flujo de pedidos.
- Cancelar un pedido
- Enviar el pedido a otra app, como una app de correo electrónico
Durante el proceso, aprenderás sobre la manera en que Android maneja las tareas y la pila de actividades para una app. Esto te permitirá manipular la pila de actividades en situaciones como cancelar un pedido, lo que lleva al usuario a la primera pantalla de la app (a diferencia de la pantalla anterior del flujo de pedido).
Requisitos previos
- Poder crear y usar un modelo de vista compartida entre fragmentos en una actividad
- Saber cómo se usa el componente de Navigation de Jetpack
- Haber usado la vinculación de datos con LiveData para mantener la IU sincronizada con el modelo de vista
- Poder crear un intent para iniciar una actividad nueva
Qué aprenderás
- Cómo afecta la navegación a la pila de actividades de una app
- Cómo implementar un comportamiento personalizado de la pila de actividades
Qué compilarás
- Una app de pedidos de cupcakes que permite al usuario enviar el pedido a otra app y que permite cancelar un pedido
Requisitos
- Una computadora que tenga Android Studio instalado
- El código de la app de Cupcake que completaste en el codelab anterior
2. Descripción general de la app de inicio
Este codelab usa la app de Cupcake del codelab anterior. Puedes usar tu código del codelab anterior o descargar el código de partida desde GitHub.
Descarga el código de partida para este codelab
Si descargas el código de partida de GitHub, ten en cuenta que el nombre de la carpeta del proyecto es android-basics-kotlin-cupcake-app-viewmodel
. Selecciona esta carpeta cuando abras el proyecto en Android Studio.
A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:
Obtén el código
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
Ahora, ejecuta la app, que debería verse de esta manera:
En este codelab, primero terminarás de implementar el botón Up en la app de manera que, cuando el usuario lo presione, vaya al paso anterior del flujo de pedido.
Luego, agregarás un botón Cancelar para que el usuario pueda cancelar el pedido si cambia de opinión durante el proceso.
Después, extenderás la app para que si presionas Send Order to Another App, se comparta el pedido con otra aplicación. Entonces, el pedido se puede enviar a una tienda de cupcakes por correo electrónico, por ejemplo.
Vamos a empezar y a completar la app de Cupcake.
3. Cómo implementar el comportamiento del botón Up
En la app de Cupcake, la barra de la app muestra una flecha para volver a la pantalla anterior. Esto se conoce como el botón Up, que aprendiste en codelabs anteriores. El botón Up no tiene ninguna acción, por lo que debes corregir primero este error de navegación en la app.
- En
MainActivity
, deberías tener código para configurar la barra de la app (también conocida como barra de acciones) con el controlador de navegación. Haz quenavController
sea una variable de clase para poder usarla en otro método.
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
}
- Dentro de la misma clase, agrega código para anular la función
onSupportNavigateUp()
. Este código le pedirá anavController
que controle la navegación hacia arriba en la app. De lo contrario, regresa a la implementación de la superclase (enAppCompatActivity
) para manejar el botón Up.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
- Ejecuta la app. El botón Up ahora debería funcionar desde
FlavorFragment
,PickupFragment
ySummaryFragment
. Cuando navegas por los pasos anteriores en el flujo de pedido, los fragmentos deben mostrar la variante correcta y la fecha de recogida desde el modelo de vista.
4. Información sobre las tareas y la pila de actividades
Ahora, incorporarás un botón Cancel en el flujo de pedido de la app. Cuando se cancela un pedido en cualquier momento del proceso, el usuario vuelve a StartFragment
. Para manejar este comportamiento, conocerás las tareas y la pila de actividades en Android.
Tareas
Las actividades en Android existen dentro de las tareas. Cuando abres una app por primera vez desde el ícono de selector, Android crea una tarea nueva con tu actividad principal. Una tarea es una colección de actividades con las que el usuario interactúa cuando realiza un trabajo determinado (por ejemplo, revisar un correo electrónico, crear un pedido de cupcake o tomar una foto).
Las actividades se organizan en una pila, conocida como pila de actividades, en la que cada actividad nueva que visita el usuario se envía a la pila de actividades para la tarea. Piensa que es una pila de panqueques, en la que se agrega cada panqueque nuevo encima de la pila. La actividad de la parte superior de la pila es la actividad actual con la que está interactuando el usuario. Las actividades debajo de la pila se ubicaron en segundo plano y se detuvieron.
La pila de actividades es útil cuando el usuario quiere navegar hacia atrás. Android puede quitar la actividad actual de la parte superior de la pila, destruirla y volver a iniciarla en la parte inferior. Se conoce como quitar una actividad de la pila y poner la actividad anterior en primer plano para que el usuario interactúe con ella. Si el usuario desea regresar varias veces, Android continuará quitando las actividades de la parte superior de la pila hasta que se acerque a la parte inferior. Cuando no hay más actividades en la pila, el usuario regresa a la pantalla del selector del dispositivo (o a la app que la inició).
Veamos la versión de la app de Words que implementaste con 2 actividades: MainActivity
y DetailActivity
.
Cuando inicias la app por primera vez, se abre MainActivity
y se agrega a la pila de actividades de la tarea.
Cuando haces clic en una letra, se inicia DetailActivity
y se envía a la pila de actividades. Esto significa que DetailActivity
se creó, se inició y se reanudó para que el usuario pueda interactuar con ella. MainActivity
se coloca en segundo plano y se muestra con el color de fondo gris en el diagrama.
Si presionas el botón Back, se quita DetailActivity
de la pila de actividades y la instancia DetailActivity
se destruye y se termina.
Luego, el elemento siguiente en la parte superior de la pila de actividades (el MainActivity
) pasa al primer plano.
De la misma manera que la pila de actividades puede llevar un seguimiento de las actividades que el usuario abrió, la pila de actividades también puede realizar un seguimiento de los destinos de fragmentos que visitó el usuario con la ayuda del componente de Navigation de Jetpack.
La biblioteca de Navigation te permite abrir un destino de fragmento fuera de la pila de actividades cada vez que el usuario presiona el botón Back. Este comportamiento predeterminado viene gratis, sin necesidad de que implementes nada. Solo debes escribir código si necesitas un comportamiento personalizado de la pila de actividades, lo que harás para la app de Cupcake.
Comportamiento predeterminado de la app de Cupcake
Veamos cómo funciona la pila de actividades en la app de Cupcake. Solo hay una actividad en la app, pero hay varios destinos de fragmentos a los que accede el usuario. Por lo tanto, se busca que el botón Back vuelva a un destino de fragmento anterior cada vez que se presiona.
Cuando abres la app por primera vez, se muestra el destino StartFragment
. Ese destino se inserta encima de la pila.
Después de seleccionar una cantidad de cupcakes para pedir, navega hasta FlavorFragment
, que se lleva a la pila de actividades.
Cuando seleccionas un tipo y presionas Next, navegas a PickupFragment
, que se inserta en la pila de actividades.
Por último, una vez que selecciones una fecha de recogida y presiones Next, navegarás a SummaryFragment
, que se agregará a la parte superior de la pila de actividades.
En el SummaryFragment
, imagina que presionas el botón Back o Up. El elemento SummaryFragment
se quita de la pila y se destruye.
El objeto PickupFragment
ahora se encuentra en la parte superior de la pila de actividades y se muestra al usuario.
Vuelve a presionar el botón Back o Up. PickupFragment
se quita de la pila y aparece FlavorFragment
.
Vuelve a presionar el botón Back o Up. FlavorFragment
se quita de la pila y aparece StartFragment
.
Cuando navegues hacia atrás a los pasos anteriores en el flujo de pedido, solo se quita un destino a la vez. En la próxima tarea, agregarás a la app la función de cancelar el pedido. En ese caso, es posible que debas quitar varios destinos de la pila de actividades a la vez para llevar al usuario a StartFragment
a fin de que inicie un pedido nuevo.
Cómo modificar la pila de actividades en la app de Cupcake
Modifica las clases FlavorFragment
, PickupFragment
y SummaryFragment
, y los archivos de diseño, para ofrecer al usuario un botón Cancel con el que pueda cancelar pedidos.
Cómo agregar una acción de navegación
Primero, agrega acciones de navegación al gráfico de navegación en tu app, de modo que el usuario pueda volver a StartFragment
desde los destinos posteriores.
- Abre Navigation Editor. Para ello, ve al archivo res > navigation > nav_graph.xml y selecciona la vista Design.
- En este momento, hay una acción de
startFragment
aflavorFragment
, una acción deflavorFragment
apickupFragment
y una acción depickupFragment
asummaryFragment
. - Haz clic y arrastra para crear una nueva acción de navegación de
summaryFragment
astartFragment
. Consulta estas instrucciones si quieres repasar cómo conectar destinos en el gráfico de navegación. - Desde
pickupFragment
, haz clic y arrastra para crear una nueva acción astartFragment
. - Desde
flavorFragment
, haz clic y arrastra para crear una nueva acción astartFragment
. - Cuando termines, el gráfico de navegación debería verse de la siguiente manera:
Con estos cambios, un usuario podría atravesar uno de los fragmentos posteriores en el flujo de pedido hasta el principio del flujo de pedido. Ahora necesitas un código que de verdad navegue con esas acciones. El lugar correcto es cuando se presiona el botón Cancel.
Cómo agregar el botón Cancel al diseño
Primero, agrega el botón Cancel a los archivos de diseño de todos los fragmentos, excepto a StartFragment
. No es necesario que canceles un pedido si ya estás en la primera pantalla del flujo de pedido.
- Abre el archivo de diseño
fragment_flavor.xml
. - Usa la vista Split para editar el XML directamente y obtener la vista previa en paralelo.
- Agrega el botón Cancel en la vista de texto subtotal y el botón Next. Asigna un ID de recurso
@+id/cancel_button
con texto para que se muestre como@string/cancel
.
El botón debe colocarse de forma horizontal junto al botón Next para que aparezcan como fila de botones. Para una restricción vertical, restringe la parte superior del botón Cancel hasta la parte superior del botón Next. En el caso de las restricciones horizontales, restringe el inicio del botón Cancel en el contenedor superior y restringe su extremo hasta el inicio del botón Next.
También otorga al botón Cancel una altura de wrap_content
y un ancho de 0dp
, para que se pueda dividir por igual el ancho de la pantalla con el otro botón. Ten en cuenta que el botón no será visible en el panel Preview hasta que el paso siguiente.
...
<TextView
android:id="@+id/subtotal" ... />
<Button
android:id="@+id/cancel_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/next_button" />
<Button
android:id="@+id/next_button" ... />
...
- En
fragment_flavor.xml
, también deberás cambiar la restricción de inicio del botón Next deapp:layout_constraintStart_toStartOf="parent
aapp:layout_constraintStart_toEndOf="@id/cancel_button"
. También agrega un margen final en el botón Cancel para que haya un espacio en blanco entre los dos botones. Ahora el botón Cancel debería aparecer en el panel Preview de Android Studio.
...
<Button
android:id="@+id/cancel_button"
android:layout_marginEnd="@dimen/side_margin" ... />
<Button
android:id="@+id/next_button"
app:layout_constraintStart_toEndOf="@id/cancel_button"... />
...
- En términos de estilo visual, aplica el estilo Material Outline Button (con el atributo
style="?attr/materialButtonOutlinedStyle"
) para que el botón Cancel no aparezca en forma destacada en comparación con Next, que es la acción principal en la que deseas que el usuario se enfoque.
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle" ... />
El botón y el posicionamiento ahora se ven excelentes.
- De la misma manera, agrega un botón Cancel al archivo de diseño
fragment_pickup.xml
.
...
<TextView
android:id="@+id/subtotal" ... />
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/side_margin"
android:text="@string/cancel"
app:layout_constraintEnd_toStartOf="@id/next_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/next_button" />
<Button
android:id="@+id/next_button" ... />
...
- Actualiza también la restricción de inicio en el botón Next. Luego, el botón Cancel aparecerá en la vista previa.
<Button
android:id="@+id/next_button"
app:layout_constraintStart_toEndOf="@id/cancel_button" ... />
- Aplica un cambio similar al archivo
fragment_summary.xml
, aunque el diseño de este fragmento es un poco diferente. Agregarás el botón Cancel debajo del botón Send en la vertical superiorLinearLayout
con algún margen intermedio.
...
<Button
android:id="@+id/send_button" ... />
<Button
android:id="@+id/cancel_button"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_between_elements"
android:text="@string/cancel" />
</LinearLayout>
- Ejecuta y prueba la app. Ahora deberías ver el botón Cancel en los diseños para
FlavorFragment
,PickupFragment
ySummaryFragment
. Sin embargo, presionar el botón aún no hace nada. Configura los objetos de escucha de clic para estos botones en el paso siguiente.
Cómo agregar el objeto de escucha de clics en el botón Cancel
Dentro de cada clase de fragmento (excepto StartFragment
), agrega un método auxiliar que se controle cuando se haga clic en el botón Cancel.
- Agrega este método
cancelOrder()
aFlavorFragment
. Cuando veas las opciones de variantes, si el usuario decide cancelar su pedido, borra el modelo de vista llamando asharedViewModel.resetOrder().
. Luego, vuelve aStartFragment
con la acción de navegación con el IDR.id.action_flavorFragment_to_startFragment.
.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_flavorFragment_to_startFragment)
}
Si ves un error relacionado con el ID de recurso de acción, es posible que debas volver al archivo nav_graph.xml
para verificar que tus acciones de navegación también tengan el mismo nombre (action_flavorFragment_to_startFragment
).
- Usa la vinculación del objeto de escucha para configurar el objeto de escucha de clics en el botón Cancel en el diseño
fragment_flavor.xml
. Si haces clic en este botón, se invocará el métodocancelOrder()
que acabas de crear en la claseFragmentFlavor
.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
- Repite el mismo proceso para la operación
PickupFragment
. Agrega un métodocancelOrder()
a la clase de fragmento, lo que restablece el orden y navega dePickupFragment
aStartFragment
.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
- En
fragment_pickup.xml
, configura el objeto de escucha de clics en el botón Cancel para llamar al métodocancelOrder()
cuando se haga clic en él.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
- Agrega un código similar para el botón Cancel en
SummaryFragment
, lo que lleva al usuario de vuelta aStartFragment
. Es posible que debas importarandroidx.navigation.fragment.findNavController
si no se importa automáticamente.
fun cancelOrder() {
sharedViewModel.resetOrder()
findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
- En
fragment_summary.xml
, llama al métodocancelOrder()
deSummaryFragment
cuando se haga clic en el botón Cancel.
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
- Ejecuta y prueba la app para verificar la lógica que acabas de agregar a cada fragmento. Cuando crees un pedido de cupcakes, presiona el botón Cancel en
FlavorFragment
,PickupFragment
oSummaryFragment
para regresar aStartFragment
. Al continuar con la creación de un pedido nuevo, notarás que la información de tu pedido anterior se eliminó.
Parece que funciona, pero hay un error relacionado con la navegación hacia atrás, una vez que regresas a StartFragment
. Sigue estos pasos para reproducir el error.
- Sigue el proceso de pedido para crear un nuevo pedido de cupcakes hasta llegar a la pantalla de resumen. Por ejemplo, puedes pedir 12 cupcakes de chocolate y elegir una fecha futura para retirar.
- Luego, presiona Cancel. Deberías regresar a
StartFragment
. - Esto parece correcto, pero si presionas el botón Back del sistema, regresas a la pantalla de resumen del pedido con un resumen de 0 cupcakes y ningún sabor. Esto es incorrecto y no debería mostrarse al usuario.
Es probable que el usuario no quiera volver a usar el flujo de pedido. Además, se eliminaron todos los datos de pedido del modelo de vista, por lo que esta información no es útil. En su lugar, presiona el botón Back de StartFragment
para salir de la app de Cupcake.
Veamos la apariencia actual de la pila de actividades y cómo solucionar el error. Cuando creas un pedido en la pantalla de resumen del pedido, cada destino se envía a la pila de actividades.
Cancelaste el pedido desde SummaryFragment
. Cuando navegaste mediante la acción de SummaryFragment
a StartFragment
, Android agregó otra instancia de StartFragment
como un destino nuevo en la pila de actividades.
Es por eso que, cuando presionaste el botón Back en StartFragment
, la app terminó mostrando SummaryFragment
otra vez (con la información del pedido en blanco).
Para corregir este error de navegación, descubre cómo el componente de Navigation te permite quitar destinos adicionales de la pila de actividades cuando navegas usando una acción.
Cómo quitar destinos adicionales de la pila de actividades
Acción de navegación: atributo popUpTo
Si incluyes un atributo app:popUpTo
en la acción de navegación del gráfico de navegación, se puede quitar más de un destino de la pila de actividades hasta que se alcance ese destino especificado. Si especificas app:popUpTo="@id/startFragment"
, los destinos de la pila de actividades aparecerán inhabilitados hasta llegar a StartFragment
, que permanecerá en la pila.
Cuando agregues este cambio a tu código y ejecutes la app, notarás que al cancelar un pedido, vuelves a StartFragment
. Sin embargo, esta vez, cuando presiones el botón Back de StartFragment
, volverás a ver StartFragment
(en lugar de salir de la app). Este no es el comportamiento deseado. Como se mencionó anteriormente, dado que estás navegando a StartFragment
, Android realmente agrega StartFragment
como un destino nuevo en la pila de actividades, por lo que ahora tienes 2 instancias de StartFragment en la pila de actividades. Por lo tanto, debes presionar dos veces el botón Back para salir de la app.
Acción de navegación: atributo popUpToInclusive
Para corregir este error nuevo, solicita que todos los destinos salgan de la pila de actividades e incluyan StartFragment
. Para ello, especifica app:popUpTo="@id/startFragment"
y app:popUpToInclusive="true"
en las acciones de navegación adecuadas. De esta manera, solo tendrás la instancia nueva de StartFragment
en la pila de actividades. A continuación, presionar el botón Back una vez desde StartFragment
cerrará la app. Hagamos este cambio ahora.
Cómo modificar las acciones de navegación
- Abre Navigation Editor. Para ello, abre el archivo es > navigation > nav_graph.xml.
- Selecciona la acción que va de
summaryFragment
astartFragment
, que está destacada en azul. - Expande la opción Attributes a la derecha (si aún no está abierta). Busca Pop Behavior en la lista de atributos que puedes modificar.
- En las opciones desplegables, configura popUpTo como
startFragment
. Esto significa que todos los destinos de la pila de actividades se cerrarán (a partir de la parte superior de la pila y hacia abajo), hastastartFragment
.
- Luego, selecciona la casilla de verificación popUpToInclusive hasta que muestre una marca de verificación y la etiqueta true. Esto indica que quieres quitar los destinos hasta la instancia de
startFragment
que ya está en la pila de actividades. Así, no tendrás dos instancias destartFragment
en la pila de actividades.
- Repite estos cambios para la acción que conecta
pickupFragment
constartFragment
.
- Repite el procedimiento para la acción que conecta
flavorFragment
constartFragment
. - Cuando hayas terminado, confirma que hayas realizado los cambios correctos en tu app observando la vista Code del archivo del gráfico de navegación.
<navigation
android:id="@+id/nav_graph" ...>
<fragment
android:id="@+id/startFragment" ...>
...
</fragment>
<fragment
android:id="@+id/flavorFragment" ...>
...
<action
android:id="@+id/action_flavorFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/pickupFragment" ...>
...
<action
android:id="@+id/action_pickupFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/summaryFragment" ...>
<action
android:id="@+id/action_summaryFragment_to_startFragment"
app:destination="@id/startFragment"
app:popUpTo="@id/startFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
Ten en cuenta que, para cada una de las 3 acciones (action_flavorFragment_to_startFragment
, action_pickupFragment_to_startFragment
y action_summaryFragment_to_startFragment
), debe haber atributos nuevos app:popUpTo="@id/startFragment"
y app:popUpToInclusive="true"
.
- Ahora, ejecuta la app. Revisa el flujo de pedidos y presiona Cancel. Cuando regreses a
StartFragment
, presiona el botón Back (solo una vez) para salir de la app.
Como resumen de lo que sucede, cuando cancelaste el pedido y regresaste a la primera pantalla de la app, todos los destinos de fragmentos de la pila de actividades se quitaron de la pila, incluida la primera instancia de StartFragment
. Después de completar la acción de navegación, se agregó StartFragment
como un destino nuevo en la pila de actividades. Al presionar Back desde allí, se muestra StartFragment
fuera de la pila y no quedan más fragmentos en la pila de actividades. Por lo tanto, Android termina la actividad y el usuario sale de la app.
La app debería tener el siguiente aspecto:
5. Envía el pedido
La app se ve fantástica hasta ahora. Sin embargo, falta una parte. Cuando presionas el botón para enviar el pedido en SummaryFragment
, sigue apareciendo un mensaje Toast
.
Sería una experiencia más útil si el pedido se pudiera enviar desde la app. Aprovecha lo que aprendiste en codelabs anteriores sobre el uso de un intent implícito para compartir información de tu app con otra. De esta manera, el usuario puede compartir la información sobre el pedido de Cupcake con una app de correo electrónico en el dispositivo, lo que permite que se envíe el pedido a la tienda de cupcakes.
Para implementar esta función, observa cómo se estructuran el asunto y el cuerpo del correo electrónico en la captura de pantalla anterior.
Deberás usar estas strings que ya están en tu archivo strings.xml
.
<string name="new_cupcake_order">New Cupcake Order</string>
<string name="order_details">Quantity: %1$s cupcakes \n Flavor: %2$s \nPickup date: %3$s \n Total: %4$s \n\n Thank you!</string>
order_details
es un recurso de string con 4 argumentos de formato diferentes, que son marcadores de posición para la cantidad real de cupcakes, la variante deseada, la fecha de recogida deseada y el precio total. Los argumentos están numerados del 1 al 4 con la sintaxis %1
a %4
. También se especifica el tipo de argumento ($s
significa que se espera una string aquí).
En el código Kotlin, podrás llamar a getString()
en R.string.order_details
seguido de los 4 argumentos (el orden es importante). A modo de ejemplo, si llamas a getString(R.string.order_details, "12", "Chocolate", "Sat Dec 12", "$24.00")
, se crea la siguiente string, que es exactamente el cuerpo de correo electrónico que deseas.
Quantity: 12 cupcakes Flavor: Chocolate Pickup date: Sat Dec 12 Total: $24.00 Thank you!
- En
SummaryFragment.kt
, modifica el métodosendOrder()
. Quita el mensajeToast
existente.
fun sendOrder() {
}
- Dentro del método
sendOrder()
, construye el texto de resumen del pedido. Para crear la string con formatoorder_details
, obtén la cantidad, la variante, la fecha y el precio del modelo de vista compartida.
val orderSummary = getString(
R.string.order_details,
sharedViewModel.quantity.value.toString(),
sharedViewModel.flavor.value.toString(),
sharedViewModel.date.value.toString(),
sharedViewModel.price.value.toString()
)
- Dentro del método
sendOrder()
, crea un intent implícito para compartir el pedido con otra app. Consulta la documentación sobre cómo crear una intent de correo electrónico. EspecificaIntent.ACTION_SEND
para la acción del intent, establece el tipo en"text/plain"
, e incluye extras de intents para el asunto del correo electrónico (Intent.EXTRA_SUBJECT
) y el cuerpo del correo electrónico (Intent.EXTRA_TEXT
). Importaandroid.content.Intent
si es necesario.
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
.putExtra(Intent.EXTRA_TEXT, orderSummary)
Como una sugerencia adicional, si adaptas esta app a tu propio caso de uso, puedes prepropagar el destinatario del correo electrónico para que sea la dirección de correo electrónico de la tienda de cupcakes. En el intent, debes especificar el destinatario del correo electrónico con el intent adicional Intent.EXTRA_EMAIL
.
- Ya que se trata de un intent implícito, no necesitas saber con anticipación qué componente o app específicos controlarán este intent. El usuario decidirá qué app quiere usar para cumplir con el intent. Sin embargo, antes de lanzar una actividad con esta intent, comprueba si hay una app que pueda manejarlo. Esta verificación evitará que la app de Cupcake falle si no hay una app que maneje el intent, lo que hace que tu código sea más seguro.
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
Para realizar esta comprobación, accede a PackageManager
, que tiene información sobre qué paquetes de la app están instalados en el dispositivo. Se puede acceder a PackageManager
a través de activity
del fragmento, siempre que activity
y packageManager
no sean nulos. Llama al método resolveActivity()
de PackageManager
con el intent que creaste. Si el resultado no es nulo, entonces es seguro llamar a startActivity()
con tu intent.
- Ejecuta la app para probar el código. Crea un pedido de cupcakes y presiona Send Order to Another App. Cuando aparece el cuadro de diálogo para compartir, puedes seleccionar la app de Gmail, pero si lo prefieres, puedes elegir otra aplicación. Si eliges la app de Gmail, puede que debas configurar una cuenta en el dispositivo si aún no lo hiciste (por ejemplo, si usas el emulador). Si no ves tu pedido de cupcakes más reciente en el cuerpo del correo electrónico, es posible que primero debas descartar el borrador actual del correo electrónico.
Cuando pruebes diferentes situaciones, es posible que veas un error si solo tienes 1 cupcake. El resumen del pedido dice 1 cupcakes, pero en inglés y español, esto es un error gramatical.
En su lugar, debería decir 1 cupcake (no plural). Si quieres elegir si la palabra "cupcake" o "cupcakes" se usa en función del valor de cantidad, puedes usar algo llamado strings de cantidad en Android. Si declaras un recurso plurals
, puedes especificar diferentes recursos de string para usar según la cantidad, por ejemplo, en singular o plural.
- Agrega un recurso de plurales
cupcakes
en el archivostrings.xml
.
<plurals name="cupcakes">
<item quantity="one">%d cupcake</item>
<item quantity="other">%d cupcakes</item>
</plurals>
En el caso singular (quantity="one"
), se usará la string singular. En todos los demás casos (quantity="other"
), se usará la string plural. Ten en cuenta que, en lugar de %s
, que espera un argumento de string, %d
espera un argumento de número entero, que pasarás al formatear la string.
En tu código Kotlin, llama a lo siguiente:
getQuantityString(R.plurals.cupcakes, 1, 1)
muestra la string 1 cupcake
getQuantityString(R.plurals.cupcakes, 6, 6)
muestra la string 6 cupcakes
getQuantityString(R.plurals.cupcakes, 0, 0)
muestra la string 0 cupcakes
- Antes de ir a tu código Kotlin, actualiza el recurso de string
order_details
enstrings.xml
para que la versión plural de cupcakes ya no se codifique en ella.
<string name="order_details">Quantity: %1$s \n Flavor: %2$s \nPickup date: %3$s \n
Total: %4$s \n\n Thank you!</string>
- En la clase
SummaryFragment
, actualiza tu métodosendOrder()
para usar la nueva string de cantidad. Sería más fácil determinar la cantidad con el modelo de vista y almacenarla en una variable. Debido a quequantity
en el modelo de vista es de tipoLiveData<Int>
, es posible quesharedViewModel.quantity.value
sea nulo. Si es nulo, usa0
como el valor predeterminado paranumberOfCupcakes
.
Agrega esto como la primera línea de código en tu método sendOrder()
.
val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
El operador elvis (?:) significa que, si la expresión de la izquierda no es nula, se puede usar. De lo contrario, si la expresión de la izquierda es nula, usa la expresión a la derecha del operador elvis (que, en este caso, es 0
).
- Luego, dale formato a la string
order_details
como lo hiciste antes. En lugar de pasarnumberOfCupcakes
como el argumento de cantidad directamente, crea la string de cupcakes con formato conresources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes)
.
El método sendOrder()
completo debe verse de la siguiente manera:
fun sendOrder() {
val numberOfCupcakes = sharedViewModel.quantity.value ?: 0
val orderSummary = getString(
R.string.order_details,
resources.getQuantityString(R.plurals.cupcakes, numberOfCupcakes, numberOfCupcakes),
sharedViewModel.flavor.value.toString(),
sharedViewModel.date.value.toString(),
sharedViewModel.price.value.toString()
)
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
.putExtra(Intent.EXTRA_TEXT, orderSummary)
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
}
- Ejecuta y prueba tu código. Comprueba que el resumen de pedidos en el cuerpo del correo electrónico muestre 1 cupcake contra 6 cupcakes o 12 cupcakes.
Con eso, completaste todas las funciones de la app de Cupcake. ¡Felicitaciones! Sin duda era una app desafiante, y realizaste importantes avances en el proceso de convertirte en desarrollador de Android. Pudiste combinar con éxito todos los conceptos que aprendiste hasta ahora, al mismo tiempo que adquiriste nuevas sugerencias de solución de problemas.
Pasos finales
Tómate un momento para limpiar tu código, lo cual es una buena práctica de codificación que aprendiste en codelabs anteriores.
- Cómo optimizar importaciones
- Cómo cambiar el formato de los archivos
- Cómo quitar el código que no se usa o que tiene comentarios
- Cómo agregar comentarios en el código cuando sea necesario
Si deseas hacer que tu app sea más accesible, pruébala con TalkBack habilitado para garantizar una experiencia del usuario fluida. Los comentarios por voz deben ayudar a transmitir el propósito de cada elemento de la pantalla, cuando corresponda. Además, asegúrate de que se pueda navegar por todos los elementos de la app con los gestos de deslizamiento.
Vuelve a verificar que los casos de uso que implementaste sean los que se esperaban en tu app final. Ejemplos:
- Los datos deben conservarse en la rotación del dispositivo (gracias al modelo de vista).
- Si presionas el botón Up o Back, la información del pedido debería aparecer correctamente en
FlavorFragment
yPickupFragment
. - Al enviar el pedido a otra app, se deberían compartir los detalles correctos del pedido.
- Si se cancela un pedido, se debería borrar toda la información relacionada.
Si encuentras algún error, corrígelo.
Muy bien. Es importante revisar el trabajo.
6. Código de solución
El código de solución para este codelab se encuentra en el proyecto que se muestra a continuación.
A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:
Obtén el código
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
7. Resumen
- Android mantiene una pila de actividades de todos los destinos que visitaste, y cada nuevo destino se coloca en la pila.
- Si presionas el botón Up o Back, puedes mostrar los destinos de la pila de actividades.
- El uso del componente de Navigation de Jetpack te permite enviar y quitar destinos de fragmentos de la pila de actividades, de manera que el comportamiento del botón Back predeterminado sea gratis.
- Especifica el atributo
app:popUpTo
en una acción en el gráfico de navegación para se quiten que los destinos de la pila de actividades hasta el que se especifica en el valor del atributo. - Especifica
app:popUpToInclusive="true"
en una acción cuando el destino especificado enapp:popUpTo
también debería salir de la pila de actividades. - Puedes crear un intent implícito para compartir contenido con una app de correo electrónico. Para ello, usa
Intent.ACTION_SEND
y propaga los intents adicionales, comoIntent.EXTRA_EMAIL
,Intent.EXTRA_SUBJECT
yIntent.EXTRA_TEXT
. - Usa un recurso
plurals
si quieres utilizar diferentes recursos de strings basados en la cantidad, como singular o plural.
8. Más información
9. Practica por tu cuenta
Extiende la app de Cupcake con tus propias variaciones en el flujo de pedidos de cupcakes. Ejemplos:
- Ofrece una variante especial que tenga ciertas condiciones especiales, como no estar disponible para retirar en el mismo día.
- Pídele al usuario su nombre para el pedido de cupcakes.
- Permite que el usuario seleccione diferentes variantes de cupcakes para su pedido si la cantidad es superior a 1 cupcake.
¿Qué áreas de tu app necesitas actualizar para cumplir con esta nueva funcionalidad?
Revisa tu trabajo
La app terminada debería ejecutarse sin errores.
10. Tarea del desafío
Usa lo que aprendiste compilando la app de Cupcake a fin de compilar una app para tu propio caso de uso. Puede ser una app para pedir pizza, sándwiches o lo que se te ocurra. Se recomienda que hagas un bosquejo de los distintos destinos de tu app antes de comenzar a implementarla.
Si quieres obtener inspiración de otras ideas de diseño, también puedes consultar la app de Shrine, que es un estudio de Material que muestra cómo adoptar los temas y los componentes de Material para tu propia marca. La app de Shrine es mucho más compleja que la de Cupcake que compilaste. En lugar de apuntar a compilar una app muy difícil, piensa en las funciones pequeñas que puedes abordar primero. Genera confianza con el tiempo con victorias graduales.
Cuando termines de crear tu propia app, comparte los elementos que creaste en las redes sociales. Usa el hashtag #LearningKotlin para que podamos verlo.