Navigation y la pila de actividades

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

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

5b0a76c50478a73f.png

  1. 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.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. 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

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

21f3eec988dcfbe9.png

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 11c34fc5e516fb1c.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. 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:

45844688c0dc69a2.png

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.

fbdc1793f9fea6da.png

Luego, agregarás un botón Cancelar para que el usuario pueda cancelar el pedido si cambia de opinión durante el proceso.

d66fdafeac1b0dcf.gif

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.

170d76b64ce78f56.png

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.

fbdc1793f9fea6da.png

  1. 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 que navController 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)
    }
}
  1. Dentro de la misma clase, agrega código para anular la función onSupportNavigateUp(). Este código le pedirá a navController que controle la navegación hacia arriba en la app. De lo contrario, regresa a la implementación de la superclase (en AppCompatActivity) para manejar el botón Up.
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}
  1. Ejecuta la app. El botón Up ahora debería funcionar desde FlavorFragment, PickupFragment y SummaryFragment. 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.

517054e483795b46.png

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.

4bc8f5aff4d5ee7f.png

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.

80f7c594ae844b84.png

Si presionas el botón Back, se quita DetailActivity de la pila de actividades y la instancia DetailActivity se destruye y se termina.

80f532af817191a4.png

Luego, el elemento siguiente en la parte superior de la pila de actividades (el MainActivity) pasa al primer plano.

85004712d2fbcdc1.png

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.

fe417ac5cbca4ce7.png

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.

cf0e80b4907d80dd.png

Después de seleccionar una cantidad de cupcakes para pedir, navega hasta FlavorFragment, que se lleva a la pila de actividades.

39081dcc3e537e1e.png

Cuando seleccionas un tipo y presionas Next, navegas a PickupFragment, que se inserta en la pila de actividades.

37dca487200f8f73.png

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.

d67689affdfae0dd.png

En el SummaryFragment, imagina que presionas el botón Back o Up. El elemento SummaryFragment se quita de la pila y se destruye.

215b93fd65754017.png

El objeto PickupFragment ahora se encuentra en la parte superior de la pila de actividades y se muestra al usuario.

37dca487200f8f73.png

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.

e3dae0f492450207.png

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.

  1. Abre Navigation Editor. Para ello, ve al archivo res > navigation > nav_graph.xml y selecciona la vista Design.
  2. En este momento, hay una acción de startFragment a flavorFragment, una acción de flavorFragment a pickupFragment y una acción de pickupFragment a summaryFragment.
  3. Haz clic y arrastra para crear una nueva acción de navegación de summaryFragment a startFragment. Consulta estas instrucciones si quieres repasar cómo conectar destinos en el gráfico de navegación.
  4. Desde pickupFragment, haz clic y arrastra para crear una nueva acción a startFragment.
  5. Desde flavorFragment, haz clic y arrastra para crear una nueva acción a startFragment.
  6. Cuando termines, el gráfico de navegación debería verse de la siguiente manera:

dcbd27a08d24cfa0.png

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.

  1. Abre el archivo de diseño fragment_flavor.xml.
  2. Usa la vista Split para editar el XML directamente y obtener la vista previa en paralelo.
  3. 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" ... />

...
  1. En fragment_flavor.xml, también deberás cambiar la restricción de inicio del botón Next de app:layout_constraintStart_toStartOf="parent a app: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"... />

...
  1. 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.

1fb41763cc255c05.png

  1. 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" ... />

...
  1. 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" ... />
  1. 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 superior LinearLayout con algún margen intermedio.

741c0f034397795c.png

...

    <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>
  1. Ejecuta y prueba la app. Ahora deberías ver el botón Cancel en los diseños para FlavorFragment, PickupFragment y SummaryFragment. 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.

  1. Agrega este método cancelOrder() a FlavorFragment. Cuando veas las opciones de variantes, si el usuario decide cancelar su pedido, borra el modelo de vista llamando a sharedViewModel.resetOrder().. Luego, vuelve a StartFragment con la acción de navegación con el ID R.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).

  1. 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étodo cancelOrder() que acabas de crear en la clase FragmentFlavor.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> flavorFragment.cancelOrder()}" ... />
  1. Repite el mismo proceso para la operación PickupFragment. Agrega un método cancelOrder() a la clase de fragmento, lo que restablece el orden y navega de PickupFragment a StartFragment.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_pickupFragment_to_startFragment)
}
  1. En fragment_pickup.xml, configura el objeto de escucha de clics en el botón Cancel para llamar al método cancelOrder() cuando se haga clic en él.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
  1. Agrega un código similar para el botón Cancel en SummaryFragment, lo que lleva al usuario de vuelta a StartFragment. Es posible que debas importar androidx.navigation.fragment.findNavController si no se importa automáticamente.
fun cancelOrder() {
    sharedViewModel.resetOrder()
    findNavController().navigate(R.id.action_summaryFragment_to_startFragment)
}
  1. En fragment_summary.xml, llama al método cancelOrder() de SummaryFragment cuando se haga clic en el botón Cancel.
<Button
    android:id="@+id/cancel_button"
    android:onClick="@{() -> summaryFragment.cancelOrder()}" ... />
  1. 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 o SummaryFragment para regresar a StartFragment. 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.

  1. 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.
  2. Luego, presiona Cancel. Deberías regresar a StartFragment.
  3. 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.

1a9024cd58a0e643.png

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.

fc88100cdf1bdd1.png

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.

5616cb0028b63602.png

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

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.

dd0fedc6e231e595.png

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.

cf0e80b4907d80dd.png

Cómo modificar las acciones de navegación

  1. Abre Navigation Editor. Para ello, abre el archivo es > navigation > nav_graph.xml.
  2. Selecciona la acción que va de summaryFragment a startFragment, que está destacada en azul.
  3. 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.

8c87589f9cc4d176.png

  1. 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), hasta startFragment.

a9a17493ed6bc27f.png

  1. 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 de startFragment en la pila de actividades.

4a403838a62ff487.png

  1. Repite estos cambios para la acción que conecta pickupFragment con startFragment.

4a403838a62ff487.png

  1. Repite el procedimiento para la acción que conecta flavorFragment con startFragment.
  2. 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".

  1. 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: 2e0599d9b55401f1.png

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.

90ed727c7b812fd6.png

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.

170d76b64ce78f56.png

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!
  1. En SummaryFragment.kt, modifica el método sendOrder(). Quita el mensaje Toast existente.
fun sendOrder() {

}
  1. Dentro del método sendOrder(), construye el texto de resumen del pedido. Para crear la string con formato order_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()
)
  1. 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. Especifica Intent.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). Importa android.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.

  1. 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.

  1. 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.

170d76b64ce78f56.png

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.

ef046a100381bb07.png

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.

  1. Agrega un recurso de plurales cupcakes en el archivo strings.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

  1. Antes de ir a tu código Kotlin, actualiza el recurso de string order_details en strings.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>
  1. En la clase SummaryFragment, actualiza tu método sendOrder() 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 que quantity en el modelo de vista es de tipo LiveData<Int>, es posible que sharedViewModel.quantity.value sea nulo. Si es nulo, usa 0 como el valor predeterminado para numberOfCupcakes.

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).

  1. Luego, dale formato a la string order_details como lo hiciste antes. En lugar de pasar numberOfCupcakes como el argumento de cantidad directamente, crea la string de cupcakes con formato con resources.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)
    }
}
  1. 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 y PickupFragment.
  • 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

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

5b0a76c50478a73f.png

  1. 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.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. 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

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.

21f3eec988dcfbe9.png

  1. En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 11c34fc5e516fb1c.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. 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 en app: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, como Intent.EXTRA_EMAIL, Intent.EXTRA_SUBJECT y Intent.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.