ViewModel compartido entre fragmentos

1. Antes de comenzar

Aprendiste cómo usar actividades, fragmentos, intents, vinculaciones de datos y componentes de navegación, y los aspectos básicos de los componentes de la arquitectura. En este codelab, reunirás todo el conocimiento y trabajarás en un ejemplo avanzado: una app para hacer pedidos de magdalenas.

Aprenderás cómo usar un elemento ViewModel compartido para compartir datos entre los fragmentos de la misma actividad y conceptos nuevos, por ejemplo, las transformaciones LiveData.

Requisitos previos

  • Estar familiarizado con la lectura y la comprensión de los diseños de Android en XML
  • Estar familiarizado con los aspectos básicos del componente de Jetpack Navigation
  • Ser capaz de crear un gráfico de navegación con destinos de fragmentos en una app
  • Haber usado previamente fragmentos dentro de una actividad
  • Ser capaz de crear un elemento ViewModel para almacenar datos de app
  • Ser capaz de usar la vinculación de datos con LiveData para mantener la IU actualizada con los datos de app en el elemento ViewModel

Qué aprenderás

  • Cómo implementar las prácticas recomendadas de la arquitectura de apps en un caso de uso más avanzado
  • Cómo usar un elemento ViewModel compartido entre fragmentos de una actividad
  • Cómo aplicar una transformación LiveData

Qué compilarás

  • Una app llamada Cupcake que muestra un flujo de pedidos de magdalenas y le permite al usuario elegir el sabor de las magdalenas, la cantidad y la fecha de retiro del pedido.

Requisitos

  • Una computadora que tenga Android Studio instalado
  • Código de inicio para la app Cupcake

2. Descripción general de la app de inicio

Descripción general de la app Cupcake

La app de magdalenas demuestra cómo diseñar e implementar una app de pedidos en línea. Al final de esta ruta de aprendizaje, habrás completado la app Cupcake con las siguientes pantallas: El usuario puede elegir la cantidad, el sabor y otras opciones para el pedido de magdalenas.

732881cfc463695d.png

Descarga el código de partida para este codelab

Este codelab te brinda el código de partida para que lo extiendas con funciones que se explicaron en este codelab. El código de partida contendrá código que ya conoces de los codelabs anteriores.

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

Explicación del código de inicio

  1. Abre el proyecto descargado en Android Studio. El nombre de la carpeta del proyecto es android-basics-kotlin-cupcake-app-starter. Luego, ejecuta la app.
  2. Busca los archivos para comprender el código de partida. Para los archivos de diseño, puedes usar la opción Split en la esquina superior derecha a fin de obtener una vista previa del diseño y el XML al mismo tiempo.
  3. Cuando compiles y ejecutes la app, notarás que esta no está completa. Los botones no hacen mucho (a excepción de mostrar un mensaje Toast), y no puedes navegar a los otros fragmentos.

A continuación, se explican los archivos importantes del proyecto.

MainActivity:

MainActivity tiene un código similar al código generado y predeterminado, que establece la vista de contenido de la actividad como activity_main.xml. Este código usa un constructor parametrizado AppCompatActivity(@LayoutRes int contentLayoutId) que toma un diseño que se aumentará como parte de super.onCreate(savedInstanceState).

El código en la clase MainActivity

class MainActivity : AppCompatActivity(R.layout.activity_main)

es igual que el siguiente código que usa el constructor AppCompatActivity predeterminado:

class MainActivity : AppCompatActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
   }
}

Diseños (carpeta res/layout):

La carpeta de recursos layout contiene archivos de diseño de actividades y fragmentos. Son archivos simples de diseño, y conoces el XML por los codelabs anteriores.

  • fragment_start.xml es la primera pantalla que se muestra en la app. Tiene una imagen de una magdalena y tres botones para elegir la cantidad de magdalenas a pedir: una magdalena, seis magdalenas y doce magdalenas.
  • fragment_flavor.xml muestra una lista de sabores de magdalenas como opciones del botón de selección con un botón Next.
  • fragment_pickup.xml brinda una opción para seleccionar el día de retiro y el botón Next para ir a la pantalla de resumen.
  • fragment_summary.xml muestra un resumen de los detalles del pedido, como la cantidad, el sabor y un botón para enviar el pedido a otra app.

Clases de fragmentos:

  • StartFragment.kt es la primera pantalla que se muestra en la app. Esta clase contiene el código de vinculación de vista y un controlador de clics para los tres botones.
  • Las clases FlavorFragment.kt, PickupFragment.kt y SummaryFragment.kt contienen, sobre todo, código estándar y un controlador de clics para el botón Next o Send Order to Another App, que muestra una notificación de advertencia.

Recursos (carpeta res):

  • La carpeta drawable contiene el elemento de magdalena de la primera pantalla y los archivos del ícono de selector.
  • navigation/nav_graph.xml contiene cuatro destinos de fragmentos (startFragment, flavorFragment, pickupFragment y summaryFragment) sin Acciones, que definirás más adelante en el codelab.
  • La carpeta values contiene los colores, las dimensiones, las strings, los estilos y los temas que se utilizan para personalizar el tema de la app. Debes estar familiarizado con estos tipos de recursos desde codelabs anteriores.

3. Cómo completar el gráfico de Navigation

En esta tarea, conectarás todas las pantallas de la app Cupcake y terminarás de implementar una navegación correcta dentro de la app.

¿Recuerdas qué necesitamos para usar el componente Navigation? Sigue esta guía a fin de repasar cómo configurar tu proyecto y tu app para lo siguiente:

  • Incluir la biblioteca de Jetpack Navigation
  • Agregar un elemento NavHost a la actividad
  • Crear un gráfico de navegación
  • Agregar destinos de fragmentos al gráfico de navegación

Cómo conectar destinos en el gráfico de navegación

  1. En Android Studio, en la ventana Project, abre res > navigation > nav_graph.xml. Cambia a la pestaña Design si todavía no se seleccionó.

28c2c94eb97e2f0.png

  1. Con este paso, se abrirá Navigation Editor para visualizar el gráfico de navegación en tu app. Deberías ver los cuatro fragmentos que ya existen en la app.

fdce89b318218ea6.png

  1. Conecta los destinos de fragmentos en el gráfico de navegación. Crea una acción de startFragment a flavorFragment, una conexión de flavorFragment a pickupFragment y una conexión de pickupFragment a summaryFragment. Si necesitas instrucciones más detalladas, sigue los pasos que se indican a continuación.
  2. Coloca el cursor sobre startFragment hasta que veas el borde gris alrededor del fragmento y el círculo gris que aparecen en el centro del borde derecho del fragmento. Haz clic en el círculo y arrastra hasta flavorFragment; luego, suelta el mouse.

d014c1b710c1088d.png

  1. Una flecha entre los dos fragmentos indica una conexión exitosa, lo que implica que podrás navegar de startFragment a flavorFragment. Esto se denomina acción de navegación y es algo que aprendiste en un codelab anterior.

65c7d993b98c9dea.png

  1. De manera similar, agrega acciones de navegación de flavorFragment a pickupFragment y de pickupFragment a summaryFragment. Cuando termines de crear las acciones de navegación, el gráfico de navegación completado debería verse de la siguiente manera.

724eb8992a1a9381.png

  1. Las tres acciones nuevas que creaste también deben reflejarse en el panel Component Tree.

e4ee54469f5ff1a4.png

  1. Cuando defines un gráfico de navegación, también deseas especificar el destino de inicio. Ahora, puedes ver que startFragment tiene un ícono pequeño de inicio al lado.

739d4ddac561c478.png

Este ícono indica que startFragment será el primer fragmento que se mostrará en el elemento NavHost. Establece esta configuración como el comportamiento deseado para nuestra app. En el futuro, siempre puedes cambiar el destino de inicio si haces clic con el botón derecho en un fragmento y seleccionas la opción de menú Set as Start Destination.

bf3cfa7841476892.png

A continuación, agregarás código para navegar de startFragment a flavorFragment cuando presiones los botones del primer fragmento, en lugar de mostrar una notificación Toast. A continuación, se muestra la referencia del diseño del fragmento de inicio. En una tarea posterior, pasarás la cantidad de magdalenas al fragmento de sabor.

867d8e4c72078f76.png

  1. En la ventana Project, abre el archivo Kotlin app > java > com.example.cupcake > StartFragment.
  2. En el método onViewCreated(), observa que los objetos de escucha de clics se configuraron en los tres botones. Cuando se presiona un botón, se llama al método orderCupcake() con la cantidad de magdalenas (1, 6 o 12 magdalenas) como su parámetro.

Código de referencia:

orderOneCupcake.setOnClickListener { orderCupcake(1) }
orderSixCupcakes.setOnClickListener { orderCupcake(6) }
orderTwelveCupcakes.setOnClickListener { orderCupcake(12) }
  1. En el método orderCupcake(), reemplaza el código que muestra la notificación de advertencia con el código para navegar al fragmento de sabor. Obtén el elemento NavController con el método findNavController() y, para llamar a navigate() en él, pasa el ID de acción, R.id.action_startFragment_to_flavorFragment. Asegúrate de que este ID de acción coincida con la acción que se declara en tu nav_graph.xml.

Reemplazar

fun orderCupcake(quantity: Int) {
    Toast.makeText(activity, "Ordered $quantity cupcake(s)", Toast.LENGTH_SHORT).show()
}

Con

fun orderCupcake(quantity: Int) {
   findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Agrega la importación import androidx.navigation.fragment.findNavController o elige entre las opciones que ofrece Android Studio.

2a087f53a77765a6.png

Cómo agregar navegación a los fragmentos de sabor y retiro

De manera similar a la tarea anterior, en esta, agregarás navegación a los otros fragmentos: los fragmentos de sabor y retiro.

3b351067bf4926b7.png

  1. Abre app > java > com.example.cupcake > FlavorFragment.kt. Ten en cuenta que goToNextScreen() es el método al que se llama dentro del objeto de escucha de clics del botón Next.
  2. En FlavorFragment.kt, dentro del método goToNextScreen(), reemplaza el código que muestra la notificación de advertencia para navegar al fragmento de retiro. Usa el ID de acción, R.id.action_flavorFragment_to_pickupFragment, y asegúrate de que este ID coincida con la acción que se declara en tu nav_graph.xml.
fun goToNextScreen() {
    findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}

Recuerda el elemento import androidx.navigation.fragment.findNavController.

  1. De manera similar, en PickupFragment.kt, dentro del método goToNextScreen(), reemplaza el código existente para navegar al fragmento de resumen.
fun goToNextScreen() {
    findNavController().navigate(R.id.action_pickupFragment_to_summaryFragment)
}

Importa el elemento androidx.navigation.fragment.findNavController.

  1. Ejecuta la app. Asegúrate de que los botones funcionen para navegar de una pantalla a otra. Es posible que la información que aparezca en cada fragmento esté incompleta, pero no te preocupes: propagarás esos fragmentos con los datos correctos en los próximos pasos.

96b33bf7a5bd8050.png

Cómo actualizar el título en la barra de la app

Mientras navegas por la app, observa el título en la barra de la app. Siempre se muestra como Cupcake.

Te recomendamos que ofrezcas una mejor experiencia del usuario para brindar un título más relevante según la funcionalidad del fragmento actual.

Cambia el título en la barra de la app (también conocida como barra de acciones) para cada fragmento con el elemento NavController y agrega un botón Up (←).

b7657cdc50cfeab0.png

  1. En MainActivity.kt, anula el método onCreate() para configurar el controlador de navegación. Obtén una instancia del elemento NavController desde el objeto NavHostFragment.
  2. Realiza una llamada a setupActionBarWithNavController(navController) y pasa la instancia de NavController. Como resultado, se mostrará un título en la barra de la app según la etiqueta de destino y el botón Up cuando no estés en un destino de nivel superior.
class MainActivity : AppCompatActivity(R.layout.activity_main) {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val navHostFragment = supportFragmentManager
                .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
        val navController = navHostFragment.navController

        setupActionBarWithNavController(navController)
    }
}
  1. Agrega las importaciones necesarias cuando Android Studio lo solicite.
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
  1. Configura los títulos de la barra de la app para cada fragmento. Abre navigation/nav_graph.xml y cambia a la pestaña Code.
  2. En nav_graph.xml, modifica el atributo android:label para cada destino de fragmento. Usa los siguientes recursos de cadenas que ya se declararon en la app de partida.

Para el fragmento de inicio, usa @string/app_name con el valor Cupcake.

Para el fragmento de sabor, usa @string/choose_flavor con el valor Choose Flavor.

Para el fragmento de retiro, usa @string/choose_pickup_date con el valor Choose Pickup Date.

Para el fragmento de resumen, usa @string/order_summary con el valor Order Summary.

<navigation ...>
    <fragment
        android:id="@+id/startFragment"
        ...
        android:label="@string/app_name" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/flavorFragment"
        ...
        android:label="@string/choose_flavor" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/pickupFragment"
        ...
        android:label="@string/choose_pickup_date" ... >
        <action ... />
    </fragment>
    <fragment
        android:id="@+id/summaryFragment"
        ...
        android:label="@string/order_summary" ... />
</navigation>
  1. Ejecuta la app. Observa que el título de la barra de la app cambia cuando navegas a cada destino de fragmento. Además, observa que ahora el botón Up (flecha ←) se muestra en la barra de la app. Si presionas este botón, no funcionará. Implementarás el comportamiento del botón Up en el siguiente codelab.

89e0ea37d4146271.png

4. Cómo crear un elemento ViewModel compartido

Continuemos con la propagación de los datos correctos en cada uno de los fragmentos. Usarás un elemento ViewModel compartido para guardar los datos de la app en un solo elemento ViewModel. Varios fragmentos en la app accederán al elemento ViewModel compartido con su alcance de actividad.

Es un caso de uso común para compartir datos entre fragmentos en la mayoría de las apps de producción. Por ejemplo, en la versión definitiva (de este codelab) de la app Cupcake (observa las siguientes capturas de pantalla), el usuario selecciona la cantidad de magdalenas en la primera pantalla y, en la segunda pantalla, se calcula y se muestra el precio en función de las cantidades de magdalenas. De manera similar, en la pantalla de resumen, también se utilizan otros datos de app, como el sabor y la fecha de retiro.

3b6a68cab0b9ee2.png

Si tienes en cuenta las características de la app, puedes deducir que sería útil almacenar esta información sobre el pedido en un solo elemento ViewModel, que se puede compartir entre los fragmentos de esta actividad. Recuerda que ViewModel forma parte de los componentes de la arquitectura de Android. Los datos de app que se guardan dentro de ViewModel se conservan durante los cambios de configuración. Para agregar un elemento ViewModel a tu app, crea una clase nueva que se extienda desde la clase ViewModel.

Cómo crear OrderViewModel

En esta tarea, crearás un elemento ViewModel compartido para la app Cupcake que se denomina OrderViewModel. Además, agregarás los datos de app como propiedades dentro de ViewModel y los métodos para actualizar y modificar los datos. Estas son las propiedades de la clase:

  • Cantidad del pedido (Integer)
  • Sabor de las magdalenas (String)
  • Fecha de retiro (String)
  • Precio (Double)

Cómo seguir las prácticas recomendadas para ViewModel

En un elemento ViewModel, lo recomendable es que no expongas los datos del modelo de vista como variables public. De lo contrario, las clases externas pueden modificar los datos de app de maneras inesperadas y crear casos límite que tu app no esperaba controlar. En su lugar, crea estas propiedades mutables private, implementa una propiedad de copia de seguridad y expón una versión inmutable public de cada propiedad si es necesario. La convención es que el nombre de las propiedades mutables private vaya precedido de un guion bajo (_).

Estos son los métodos para actualizar las propiedades anteriores, que dependen de la elección del usuario:

  • setQuantity(numberCupcakes: Int)
  • setFlavor(desiredFlavor: String)
  • setDate(pickupDate: String)

No necesitas un método set para el precio, ya que lo calcularás dentro del elemento OrderViewModel con otras propiedades. En los siguientes pasos, se explica cómo implementar el elemento ViewModel compartido.

Crearás un paquete nuevo en tu proyecto llamado model y agregarás la clase OrderViewModel. De esta manera, se separará el código del modelo de vista del resto del código de la IU (fragmentos y actividades). Una práctica recomendada de codificación es separar el código en paquetes según la funcionalidad.

  1. En la ventana Project de Android Studio, haz clic con el botón derecho en com.example.cupcake > New > Package.
  2. Se abrirá un diálogo New Package. Asígnale al paquete el nombre com.example.cupcake.model.

d958ee5f3d2aef5a.png

  1. Crea la clase Kotlin OrderViewModel en el paquete model. En la ventana Project, haz clic con el botón derecho en el paquete model y selecciona New > Kotlin File/Class. En el diálogo nuevo, asigna el nombre de archivo OrderViewModel.

fc68c1d3861f1cca.png

  1. En OrderViewModel.kt, cambia la firma de clase para extender desde ViewModel.
import androidx.lifecycle.ViewModel

class OrderViewModel : ViewModel() {

}
  1. Dentro de la clase OrderViewModel, agrega las propiedades que se mencionaron anteriormente como private val.
  2. Cambia los tipos de propiedades a LiveData y agrega campos de copia de seguridad a las propiedades para que estas sean observables y para que se pueda actualizar la IU cuando cambien los datos de origen en el modelo de vista.
private val _quantity = MutableLiveData<Int>(0)
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>("")
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>("")
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>(0.0)
val price: LiveData<Double> = _price

Deberás importar estas clases:

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
  1. En la clase OrderViewModel, agrega los métodos que se mencionaron anteriormente. Dentro de los métodos, asigna el argumento que se pasó a las propiedades mutables.
  2. Como se debe llamar a estos métodos de set desde fuera del modelo de vista, déjalos como métodos public (es decir, no se necesita private ni otro modificador de visibilidad antes de la palabra clave fun). El modificador predeterminado de visibilidad de Kotlin es public.
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
}

fun setFlavor(desiredFlavor: String) {
    _flavor.value = desiredFlavor
}

fun setDate(pickupDate: String) {
    _date.value = pickupDate
}
  1. Compila y ejecuta tu app para asegurarte de que no haya errores de compilación. Sin embargo, no debería haber cambios visibles en la IU.

¡Buen trabajo! Ahora, puedes comenzar con tu modelo de vista. De manera gradual, agregarás más a esta clase a medida que desarrolles más características en tu app y te des cuenta de que necesitas más propiedades y métodos.

Se espera que puedas ver los nombres de la clase, las propiedades o los métodos en la fuente gris de Android Studio, lo que implica que la clase, las propiedades o los métodos no se usan en este momento, pero se usarán más adelante. Este es el tema que se trata a continuación.

5. Cómo usar ViewModel para actualizar la IU

En esta tarea, usarás el modelo de vista compartido que creaste para actualizar la IU de la app. La principal diferencia en la implementación de un modelo de vista compartido es la manera en que accedemos a este desde los controladores de la IU. Usarás la instancia de actividad en lugar de la instancia de fragmento y verás cómo hacerlo en las siguientes secciones.

Eso significa que el modelo de vista se puede compartir entre fragmentos. Cada fragmento puede acceder al modelo de vista para verificar algunos detalles del pedido o actualizar algunos datos en el modelo de vista.

Cómo actualizar StartFragment para usar el modelo de vista

Para usar el modelo de vista compartido en StartFragment, debes inicializar el elemento OrderViewModel con activityViewModels() en lugar de la clase delegada viewModels().

  • viewModels() te brinda la instancia ViewModel con alcance para el fragmento actual. Será diferente para los distintos fragmentos.
  • activityViewModels() te brinda la instancia ViewModel con alcance para la actividad actual. Por lo tanto, la instancia permanecerá igual en varios fragmentos en la misma actividad.

Cómo usar el delegado de propiedad de Kotlin

En Kotlin, cada propiedad mutable (var) tiene funciones del método get y el método set que se generan automáticamente para ella. Se llama a las funciones del método get y el método set cuando asignas un valor o lees el valor de la propiedad. (Para una propiedad de solo lectura (val), solo la función del método get se genera de forma predeterminada. Se llama a esta función de método get cuando lees el valor de una propiedad de solo lectura).

La delegación de propiedades en Kotlin te permite transferir la responsabilidad del método get y el método set a una clase diferente.

Esta clase (que se denomina clase de delegado) brinda funciones del método get y el método set de la propiedad y controla sus cambios.

Una propiedad de delegado se define mediante la cláusula by y una instancia de clase delegada:

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
  1. En la clase StartFragment, obtén una referencia al modelo de vista compartido como una variable de clase. Usa el delegado de propiedades by activityViewModels() de Kotlin de la biblioteca fragment-ktx.
private val sharedViewModel: OrderViewModel by activityViewModels()

Es posible que necesites estas importaciones nuevas:

import androidx.fragment.app.activityViewModels
import com.example.cupcake.model.OrderViewModel
  1. Repite el paso anterior para las clases FlavorFragment, PickupFragment y SummaryFragment. Usarás esta instancia sharedViewModel en secciones posteriores del codelab.
  2. Si regresas a la clase StartFragment, ahora puedes usar el modelo de vista. Al principio del método orderCupcake(), llama al método setQuantity() en el modelo de vista compartido para actualizar la cantidad antes de navegar al fragmento de sabor.
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Dentro de la clase OrderViewModel, agrega el siguiente método para verificar si se configuró o no el sabor del pedido. Usarás este método en la clase StartFragment en un paso posterior.
fun hasNoFlavorSet(): Boolean {
    return _flavor.value.isNullOrEmpty()
}
  1. En la clase StartFragment, dentro del método orderCupcake(), después de configurar la cantidad, establece el sabor predeterminado como Vanilla, si no se estableció ningún sabor, antes de navegar al fragmento de sabor. Tu método completo se verá de la siguiente manera:
fun orderCupcake(quantity: Int) {
    sharedViewModel.setQuantity(quantity)
    if (sharedViewModel.hasNoFlavorSet()) {
        sharedViewModel.setFlavor(getString(R.string.vanilla))
    }
    findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
  1. Compila la app para asegurarte de que no haya errores de compilación. Sin embargo, no debería haber cambios visibles en la IU.

6. Cómo usar ViewModel con vinculación de datos

A continuación, usarás la vinculación de datos para asociar los datos del modelo de vista con la IU. También, actualizarás el modelo de vista compartido en función de las selecciones que el usuario realice en la IU.

Repaso sobre la vinculación de datos

Recuerda que la biblioteca de vinculación de datos forma parte de Android Jetpack. La vinculación de datos asocia los componentes de la IU en tus diseños con las fuentes de datos en tu app mediante un formato declarativo. En términos más simples, la vinculación de datos asocia datos (del código) con las vistas y la vinculación de vista (vinculación de vistas con el código). Configurar estas vinculaciones y establecer la actualización automática te permite reducir la posibilidad de que se produzcan errores si olvidas actualizar manualmente la IU desde tu código.

Cómo actualizar el sabor con la elección del usuario

  1. En layout/fragment_flavor.xml, agrega una etiqueta <data> dentro de la etiqueta <layout> raíz. Agrega una variable de diseño llamada viewModel del tipo com.example.cupcake.model.OrderViewModel. Asegúrate de que el nombre del paquete en el atributo del tipo coincida con el nombre del paquete de la clase del modelo de vista compartido, OrderViewModel, en tu app.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. De manera similar, repite el paso anterior para fragment_pickup.xml y fragment_summary.xml a fin de agregar la variable de diseño viewModel. Usarás esta variable en secciones posteriores. No es necesario que agregues este código en fragment_start.xml, ya que este diseño no usa el modelo de vista compartido.
  2. En la clase FlavorFragment, dentro de onViewCreated(), vincula la instancia del modelo de vista con la instancia del modelo de vista compartido en el diseño. Agrega el siguiente código dentro del bloque binding?.apply.
binding?.apply {
    viewModel = sharedViewModel
    ...
}

Cómo aplicar la función de alcance

Es posible que sea la primera vez que veas la función apply en Kotlin. apply es una función de alcance en la biblioteca estándar de Kotlin. Ejecuta un bloque de código dentro del contexto de un objeto. Forma un alcance temporal y, en ese alcance, puedes acceder al objeto sin su nombre. El caso de uso común para apply es configurar un objeto. Estas llamadas se pueden leer como "aplicar las siguientes asignaciones al objeto".

Ejemplo:

clark.apply {
    firstName = "Clark"
    lastName = "James"
    age = 18
}

// The equivalent code without apply scope function would look like the following.

clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
  1. Repite el mismo paso para el método onViewCreated() dentro de las clases PickupFragment y SummaryFragment.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. En fragment_flavor.xml, usa la variable nueva de diseño, viewModel, para configurar el atributo checked de los botones de selección que se basan en el valor flavor en el modelo de vista. Si el sabor que representa un botón de selección es el mismo que se guardó en el modelo de vista, muestra el botón de selección de la manera en que se seleccionó (checked = verdadero). La expresión de vinculación para el estado activado de Vanilla RadioButton se vería de la siguiente manera:

@{viewModel.flavor.equals(@string/vanilla)}

En esencia, estás comparando la propiedad viewModel.flavor con el recurso de strings correspondiente mediante la función equals para determinar si el estado activado es verdadero o falso.

<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:checked="@{viewModel.flavor.equals(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:checked="@{viewModel.flavor.equals(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:checked="@{viewModel.flavor.equals(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:checked="@{viewModel.flavor.equals(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:checked="@{viewModel.flavor.equals(@string/coffee)}"
       .../>
</RadioGroup>

Vinculaciones de objetos de escucha

Las vinculaciones de objetos de escucha son expresiones lambda que se ejecutan cuando se produce un evento, por ejemplo, un evento onClick. Son similares a las referencias de métodos, como textview.setOnClickListener(clickListener), pero las vinculaciones de objetos de escucha te permiten ejecutar expresiones arbitrarias de vinculación de datos.

  1. En fragment_flavor.xml, agrega objetos de escucha de eventos a los botones de selección mediante vinculaciones de objetos de escucha. Usa una expresión lambda sin parámetros y, para realizar una llamada al método viewModel.setFlavor(), pasa el recurso de strings del sabor correspondiente.
<RadioGroup
   ...>

   <RadioButton
       android:id="@+id/vanilla"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
       .../>

   <RadioButton
       android:id="@+id/chocolate"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
       .../>

   <RadioButton
       android:id="@+id/red_velvet"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/red_velvet)}"
       .../>

   <RadioButton
       android:id="@+id/salted_caramel"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/salted_caramel)}"
       .../>

   <RadioButton
       android:id="@+id/coffee"
       ...
       android:onClick="@{() -> viewModel.setFlavor(@string/coffee)}"
       .../>
</RadioGroup>
  1. Ejecuta la app y observa cómo se selecciona la opción Vanilla de forma predeterminada en el fragmento de sabor.

3095e824b4817b98.png

¡Excelente! Ahora, puedes continuar con los siguientes fragmentos.

7. Cómo actualizar el fragmento de resumen y retiro para usar el modelo de vista

Navega por la app y observa que, en el fragmento de retiro, las etiquetas de la opción del botón de selección están en blanco. En esta tarea, calcularás las 4 fechas de retiro disponibles y las mostrarás en el fragmento de retiro. Existen maneras diferentes de mostrar una fecha con formato, y estas son algunas de las utilidades prácticas que brinda Android para mostrarlas.

Cómo crear la lista de opciones de retiro

Formateador de fechas

El framework de Android brinda una clase denominada SimpleDateFormat, que es una clase para formatear y analizar fechas de una manera que tiene en cuenta la configuración regional. Permite dar formato a las fechas (fecha → texto) y analizar (texto → fecha) las fechas.

Para poder crear una instancia de SimpleDateFormat, pasa una string de patrón y una configuración regional:

SimpleDateFormat("E MMM d", Locale.getDefault())

Una string de patrón como "E MMM d" es una representación de los formatos de fecha y hora. Las letras de 'A' a 'Z' y de 'a' a 'z' se interpretan como letras de patrón que representan los componentes de una string de fecha u hora. Por ejemplo, d representa el día en un mes; y, el año; y M, el mes. Si la fecha es el 4 de enero de 2018, la string de patrón "EEE, MMM d" la analiza como "Wed, Jul 4". Para obtener una lista completa de las letras de patrón, consulta la documentación.

Un objeto Locale representa una región geográfica, política o cultural específica. Representa una combinación de idioma, país y variante. Las configuraciones regionales se usan para alterar la presentación de la información, como los números o las fechas, de manera que se adapten a las convenciones de la región. Para la fecha y la hora, se tiene en cuenta la configuración regional, ya que se escriben de forma diferente en distintas partes del mundo. Usarás el método Locale.getDefault() para recuperar la información de la configuración regional que se estableció en el dispositivo del usuario y pasarla al constructor SimpleDateFormat.

La configuración regional en Android es una combinación de código de idioma y país. Los códigos de idioma son los códigos de idioma ISO de dos letras minúsculas, como "en" para inglés. Los códigos de país son los códigos de país ISO de dos letras mayúsculas, como "US" para Estados Unidos.

Ahora, usa SimpleDateFormat y Locale a fin de determinar las fechas de retiro disponibles para la app Cupcake.

  1. En la clase OrderViewModel, agrega la siguiente función denominada getPickupOptions() para crear y mostrar la lista de fechas de retiro. Dentro del método, crea una variable val llamada options y, luego, inicialízala en mutableListOf<String>().
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
}
  1. Crea una string de formateador con SimpleDateFormat que pase la string del patrón "E MMM d", además de la configuración regional. En la string de patrón, E representa el nombre del día de la semana y se analiza como "Tue Dec 10".
val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())

Importa java.text.SimpleDateFormat y java.util.Locale cuando Android Studio lo solicite.

  1. Obtén una instancia Calendar y asígnala a una variable nueva. Configúrala como val. Esta variable contendrá la fecha y la hora actuales. También importa java.util.Calendar.
val calendar = Calendar.getInstance()
  1. Crea una lista de fechas que comiencen con la fecha actual y las siguientes tres fechas. Como necesitarás 4 opciones de fecha, repite este bloque de código 4 veces. Este bloque repeat le dará formato a una fecha, la agregará a la lista de opciones de fecha y aumentará el calendario en 1 día.
repeat(4) {
    options.add(formatter.format(calendar.time))
    calendar.add(Calendar.DATE, 1)
}
  1. Muestra el elemento options actualizado al final del método. Este es el método completo:
private fun getPickupOptions(): List<String> {
   val options = mutableListOf<String>()
   val formatter = SimpleDateFormat("E MMM d", Locale.getDefault())
   val calendar = Calendar.getInstance()
   // Create a list of dates starting with the current date and the following 3 dates
   repeat(4) {
       options.add(formatter.format(calendar.time))
       calendar.add(Calendar.DATE, 1)
   }
   return options
}
  1. En la clase OrderViewModel, agrega una propiedad de clase llamada dateOptions que se haya configurado como val. Inicialízala con el método getPickupOptions() que recién creaste.
val dateOptions = getPickupOptions()

Cómo actualizar el diseño para mostrar opciones de retiro

Ahora que tienes las cuatro fechas de retiro disponibles en el modelo de vista, actualiza el diseño fragment_pickup.xml para mostrar esas fechas. También usarás la vinculación de datos para mostrar el estado activado de cada botón de selección y para actualizar la fecha en el modelo de vista cuando se seleccione un botón de selección diferente. Esta implementación es similar a la vinculación de datos en el fragmento de sabor.

En fragment_pickup.xml:

El botón de selección option0 representa a dateOptions[0] en viewModel (hoy)

El botón de selección option1 representa a dateOptions[1] en viewModel (mañana)

El botón de selección option2 representa a dateOptions[2] en viewModel (el día después de mañana)

El botón de selección option3 representa a dateOptions[3] en viewModel (dos días después de mañana)

  1. En fragment_pickup.xml, para el botón de selección option0, usa la variable nueva de diseño, viewModel, a fin de configurar el atributo checked según el valor date en el modelo de vista. Compara la propiedad viewModel.date con la primera string en la lista dateOptions, que representa la fecha actual. Usa la función equals para comparar; la expresión de vinculación final se verá de la siguiente manera:

@{viewModel.date.equals(viewModel.dateOptions[0])}

  1. Para el mismo botón de selección, agrega un objeto de escucha de eventos mediante la vinculación de objetos de escucha con el atributo onClick. Cuando se haga clic en esta opción del botón de selección, realiza una llamada a setDate() en viewModel y pasa dateOptions[0].
  2. Para el mismo botón de selección, configura el valor del atributo text en la primera string de la lista dateOptions.
<RadioButton
   android:id="@+id/option0"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[0])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[0])}"
   android:text="@{viewModel.dateOptions[0]}"
   ...
   />
  1. Repite los pasos anteriores para los otros botones de selección y cambia el índice de dateOptions según corresponda.
<RadioButton
   android:id="@+id/option1"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[1])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[1])}"
   android:text="@{viewModel.dateOptions[1]}"
   ... />

<RadioButton
   android:id="@+id/option2"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[2])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[2])}"
   android:text="@{viewModel.dateOptions[2]}"
   ... />

<RadioButton
   android:id="@+id/option3"
   ...
   android:checked="@{viewModel.date.equals(viewModel.dateOptions[3])}"
   android:onClick="@{() -> viewModel.setDate(viewModel.dateOptions[3])}"
   android:text="@{viewModel.dateOptions[3]}"
   ... />
  1. Ejecuta la app. Deberías ver los siguientes días como opciones de retiro disponibles. La captura de pantalla variará en función de cuál sea el día actual. Observa que no se seleccionó ninguna opción de forma predeterminada. Implementarás esto en el siguiente paso.

b55b3a36e2aa7be6.png

  1. Dentro de la clase OrderViewModel, crea una función llamada resetOrder() para restablecer las propiedades MutableLiveData en el modelo de vista. Asigna el valor de la fecha actual de la lista dateOptions a _date.value.
fun resetOrder() {
   _quantity.value = 0
   _flavor.value = ""
   _date.value = dateOptions[0]
   _price.value = 0.0
}
  1. Agrega un bloque init a la clase y llama al método nuevo resetOrder() desde este.
init {
   resetOrder()
}
  1. Quita los valores iniciales de la declaración de las propiedades en la clase. Ahora, usa el bloque init para inicializar las propiedades cuando se cree una instancia de OrderViewModel.
private val _quantity = MutableLiveData<Int>()
val quantity: LiveData<Int> = _quantity

private val _flavor = MutableLiveData<String>()
val flavor: LiveData<String> = _flavor

private val _date = MutableLiveData<String>()
val date: LiveData<String> = _date

private val _price = MutableLiveData<Double>()
val price: LiveData<Double> = _price
  1. Vuelve a ejecutar la app y observa que la fecha actual se seleccionó de forma predeterminada.

bfe4f1b82977b4bc.png

Cómo actualizar el fragmento de resumen para usar el modelo de vista

Ahora, continuemos con el último fragmento. El fragmento de resumen del pedido se diseñó para mostrar un resumen de los detalles del pedido. En esta tarea, aprovecharás toda la información sobre el pedido del modelo de vista compartido y actualizarás los detalles del pedido en pantalla mediante la vinculación de datos.

78f510e10d848dd2.png

  1. En fragment_summary.xml, asegúrate de que se haya declarado la variable de los datos del modelo de vista, viewModel.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. En SummaryFragment, en onViewCreated(), asegúrate de que se inicialice binding.viewModel.
  2. En fragment_summary.xml, lee desde el modelo de vista para actualizar la pantalla con los detalles del resumen del pedido. Actualiza el elemento TextViews de la cantidad, el sabor y la fecha. Para ello, agrega los siguientes atributos de texto. La cantidad es del tipo Int, por lo que necesitas convertirla en una string.
<TextView
   android:id="@+id/quantity"
   ...
   android:text="@{viewModel.quantity.toString()}"
   ... />
<TextView
   android:id="@+id/flavor"
   ...
   android:text="@{viewModel.flavor}"
   ... />
<TextView
   android:id="@+id/date"
   ...
   android:text="@{viewModel.date}"
   ... />
  1. Ejecuta y prueba la app para verificar que las opciones del pedido que seleccionaste se muestren en el resumen del pedido.

7091453fa817b55.png

8. Cómo calcular el precio a partir de los detalles del pedido

En las capturas de pantalla de la app definitiva de este codelab, notarás que, en realidad, el precio se muestra en cada fragmento (a excepción de StartFragment) para que el usuario sepa el precio mientras crea el pedido.

3b6a68cab0b9ee2.png

Estas son las reglas de nuestra tienda de magdalenas para calcular el precio.

  • Cada magdalena cuesta $2.00
  • Para retirar en el mismo día, se agrega al pedido un costo adicional de $3.00

Por lo tanto, para un pedido de 6 magdalenas, el precio sería 6 magdalenas x $2 cada una = $12. Si el usuario desea retirar en el mismo día, el costo adicional de $3 daría como resultado un precio total del pedido de $15.

Cómo actualizar el precio en el modelo de vista

Para agregar compatibilidad con esta funcionalidad en tu app, primero, debes calcular el precio por magdalena y, por ahora, ignorar el costo del retiro en el mismo día.

  1. Abre OrderViewModel.kt y almacena el precio por magdalena en una variable. Declárala como una constante privada de nivel superior en la parte superior del archivo, fuera de la definición de la clase (pero después de las instrucciones de importación). Usa el modificador const y, para configurarlo como de solo lectura, usa val.
package ...

import ...

private const val PRICE_PER_CUPCAKE = 2.00

class OrderViewModel : ViewModel() {
    ...

Volver a recopilar los valores constantes (que se marcan con la palabra clave const en Kotlin) no cambia, y el valor se conoce en el tiempo de compilación. Para obtener más información acerca de las constantes, consulta la documentación.

  1. Ahora que definiste el precio por magdalena, crea un método asistente para calcular el precio. Este método puede ser private, ya que solo se usa dentro de esta clase. En la próxima tarea, cambiarás la lógica del precio, de modo que puedas incluir cargos para el retiro en el mismo día.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
}

Esta línea de código multiplica el precio de cada magdalena por la cantidad de magdalenas en el pedido. Para el código entre paréntesis, ya que el valor de quantity.value podría ser nulo, usa un operador elvis (?:). 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. En la misma clase OrderViewModel, actualiza la variable de precio cuando se configure la cantidad. Realiza una llamada a la función nueva en la función setQuantity().
fun setQuantity(numberCupcakes: Int) {
    _quantity.value = numberCupcakes
    updatePrice()
}

Cómo vincular la propiedad de precio con la IU

  1. En los diseños para fragment_flavor.xml, fragment_pickup.xml y fragment_summary.xml, asegúrate de que se defina la variable de datos viewModel del tipo com.example.cupcake.model.OrderViewModel.
<layout ...>

    <data>
        <variable
            name="viewModel"
            type="com.example.cupcake.model.OrderViewModel" />
    </data>

    <ScrollView ...>

    ...
  1. En el método onViewCreated() de cada clase de fragmento, asegúrate de vincular la instancia del objeto del modelo de vista en el fragmento con la variable de datos del modelo de vista en el diseño.
binding?.apply {
    viewModel = sharedViewModel
    ...
}
  1. Dentro de cada diseño de fragmento, usa la variable viewModel para configurar el precio si se muestra en el diseño. Primero, modifica el archivo fragment_flavor.xml. Para la vista de texto subtotal, configura el valor del atributo android:text como "@{@string/subtotal_price(viewModel.price)}".. Esta expresión del diseño de la vinculación de datos usa el recurso de strings @string/subtotal_price y pasa un parámetro, que es el precio del modelo de vista, por lo que el resultado mostrará, por ejemplo, Subtotal 12.0.
...

<TextView
    android:id="@+id/subtotal"
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

Estás usando este recurso de strings que ya se declaró en el archivo strings.xml:

<string name="subtotal_price">Subtotal %s</string>
  1. Ejecuta la app. Si seleccionas One cupcake en el fragmento de inicio, en el fragmento de sabor, se mostrará Subtotal 2.0. Si seleccionas Six cupcakes, en el fragmento de sabor, se mostrará Subtotal 12.0, etc. Más adelante, le darás al precio el formato correcto de moneda, por lo que, por ahora, se espera este comportamiento.

  1. Ahora, realiza un cambio similar para los fragmentos de retiro y resumen. En los diseños fragment_pickup.xml y fragment_summary.xml, modifica las vistas de texto para usar también la propiedad viewModel price.

fragment_pickup.xml

...

<TextView
    android:id="@+id/subtotal"
    ...
    android:text="@{@string/subtotal_price(viewModel.price)}"
    ... />

...

fragment_summary.xml

<TextView
   android:id="@+id/total"
   ...
   android:text="@{@string/total_price(viewModel.price)}"
   ... />

  1. Ejecuta la app. Asegúrate de que el precio que se muestre en el resumen del pedido se calcule de manera correcta para una cantidad de pedido de 1, 6 y 12 magdalenas. Como ya se mencionó, se espera que el formato del precio no sea correcto en este momento (se mostrará 2.0 en lugar de $2 o 12.0 en lugar de $12).

Cómo cobrar un importe adicional para retiro en el mismo día

En esta tarea, implementarás la segunda regla: para retirar en el mismo día, se agrega al pedido un costo adicional de $3.00.

  1. En la clase OrderViewModel, define una nueva constante privada de nivel superior para el costo por retiro en el mismo día.
private const val PRICE_FOR_SAME_DAY_PICKUP = 3.00
  1. En updatePrice(), verifica si el usuario seleccionó el retiro en el mismo día. Verifica si la fecha del modelo de vista (_date.value) es igual al primer elemento en la lista dateOptions, que siempre es el día actual.
private fun updatePrice() {
    _price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    if (dateOptions[0] == _date.value) {

    }
}
  1. Para simplificar estos cálculos, introduce una variable temporal, calculatedPrice. Calcula el precio actualizado y vuelve a asignarlo a _price.value.
private fun updatePrice() {
    var calculatedPrice = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
    // If the user selected the first option (today) for pickup, add the surcharge
    if (dateOptions[0] == _date.value) {
        calculatedPrice += PRICE_FOR_SAME_DAY_PICKUP
    }
    _price.value = calculatedPrice
}
  1. Llama al método auxiliar updatePrice() desde setDate() para agregar los cargos por retiro en el mismo día.
fun setDate(pickupDate: String) {
    _date.value = pickupDate
    updatePrice()
}
  1. Ejecuta tu app y navega por esta. Observa que cambiar la fecha de retiro no quita del precio total los cargos por retiro en el mismo día, ya que se modifica el precio en el modelo de vista, pero no se notifica al diseño de vinculación.

2ea8e000fb4e6ec8.png

Cómo configurar el propietario del ciclo de vida para observar LiveData

LifecycleOwner es una clase que tiene un ciclo de vida de Android, por ejemplo, una actividad o un fragmento. Un observador LiveData ve los cambios en los datos de la app solo si el propietario del ciclo de vida está en estado activo (STARTED o RESUMED).

En tu app, el objeto LiveData o los datos observables son la propiedad price en el modelo de vista. Los propietarios del ciclo de vida son los fragmentos de sabor, retiro y resumen. Los observadores LiveData son las expresiones de vinculación en archivos de diseño con datos observables, por ejemplo, el precio. Con la vinculación de datos, cuando cambia un valor observable, los elementos de la IU a los que está vinculado se actualizan automáticamente.

Ejemplo de expresión de vinculación: android:text="@{@string/subtotal_price(viewModel.price)}"

Para que los elementos de la IU se actualicen automáticamente, debes asociar binding.lifecycleOwner

con los propietarios del ciclo de vida en la app. Implementarás esto a continuación.

  1. En las clases FlavorFragment, PickupFragment y SummaryFragment, dentro del método onViewCreated(), agrega lo siguiente en el bloque binding?.apply. De esta manera, se configurará el propietario del ciclo de vida en el objeto de vinculación. Si se configura el propietario del ciclo de vida, la app podrá observar objetos LiveData.
binding?.apply {
    lifecycleOwner = viewLifecycleOwner
    ...
}
  1. Vuelve a ejecutar tu app. En la pantalla de retiro, cambia la fecha de retiro y observa la diferencia en cómo el precio cambia automáticamente. Además, los cargos de retiro se reflejan de manera correcta en la pantalla de resumen.
  2. Ten en cuenta que, cuando seleccionas la fecha de hoy para retirar el pedido, el precio de este aumenta a $3.00. El precio por seleccionar cualquier fecha futura debe seguir siendo el resultado de la cantidad de magdalenas x $2.00.

  1. Prueba varios casos con diferentes cantidades de magdalenas, sabores y fechas de retiro. Ahora, deberías ver el precio que se actualiza desde el modelo de vista en cada fragmento. La mejor parte es que no fue necesario que escribas código Kotlin adicional para mantener la IU actualizada con el precio cada vez.

f4c0a3c5ea916d03.png

Para terminar de implementar la característica de precio, deberás darle al precio el formato de moneda local.

Cómo darle formato al precio con la transformación LiveData

Los métodos de transformación LiveData brindan una manera de realizar manipulaciones de datos en la fuente LiveData y mostrar un objeto LiveData resultante. En términos simples, transforman el valor de LiveData en otro valor. Estas transformaciones no se calculan a menos que un observador esté viendo el objeto LiveData.

Transformations.map() es una de las funciones de transformación. Este método toma la fuente LiveData y una función como parámetros. La función manipula la fuente LiveData y muestra un valor actualizado que también es observable.

En algunos ejemplos en tiempo real, puedes usar una transformación LiveData:

  • Dar formato a fechas y strings de tiempo para mostrarlas
  • Ordenar una lista de elementos
  • Filtrar o agrupar artículos
  • Calcular el resultado de una lista, por ejemplo, la suma de todos los elementos, la cantidad de elementos, la devolución del último elemento, etc.

En esta tarea, usarás el método Transformations.map() para darle formato al precio a fin de usar la moneda local. Transformarás el precio original como un valor decimal (LiveData<Double>) en un valor de string (LiveData<String>).

  1. En la clase OrderViewModel, cambia el tipo de propiedad de copia de seguridad a LiveData<String> en lugar de LiveData<Double>.. El precio con formato será una string con un símbolo de moneda, como "$". Corrige el error de inicialización en el paso siguiente.
private val _price = MutableLiveData<Double>()
val price: LiveData<String>
  1. Usa Transformations.map() para inicializar la variable nueva, pasa el elemento _price y una función lambda. Usa el método getCurrencyInstance() en la clase NumberFormat para convertir el precio al formato de moneda local. El código de transformación se verá de la siguiente manera:
private val _price = MutableLiveData<Double>()
val price: LiveData<String> = Transformations.map(_price) {
   NumberFormat.getCurrencyInstance().format(it)
}

Deberás importar androidx.lifecycle.Transformations y java.text.NumberFormat.

  1. Ejecuta la app. Ahora, deberías ver la string de precio con formato para el subtotal y el total. ¡Es mucho más fácil de usar!

1853bd13a07f1bc7.png

  1. Prueba que funcione según lo previsto. Prueba casos, por ejemplo: pedir una magdalena, pedir seis magdalenas y pedir 12 magdalenas. Asegúrate de que el precio se actualice de manera correcta en cada pantalla. Debería decir Subtotal $2.00 para los fragmentos de sabor y retiro, y Total $2.00 para el resumen del pedido. Además, asegúrate de que el resumen del pedido muestre los detalles correctos del pedido.

9. Cómo configurar objetos de escucha de clics con la vinculación de objetos de escucha

En esta tarea, usarás la vinculación de objetos de escucha para asociar los objetos de escucha de clics del botón en las clases de fragmentos con el diseño.

  1. En el archivo de diseño fragment_start.xml, agrega una variable de datos llamada startFragment del tipo com.example.cupcake.StartFragment. Asegúrate de que el nombre del paquete del fragmento coincida con el nombre del paquete de tu app.
<layout ...>

    <data>
        <variable
            name="startFragment"
            type="com.example.cupcake.StartFragment" />
    </data>
    ...
    <ScrollView ...>
  1. En StartFragment.kt, en el método onViewCreated(), vincula la variable nueva de datos con la instancia del fragmento. Puedes acceder a la instancia del fragmento dentro del fragmento con la palabra clave this. Quita el bloque binding?.apply junto con el código que incluye. El método completo debería verse de la siguiente manera:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding?.startFragment = this
}
  1. En fragment_start.xml, agrega objetos de escucha de eventos mediante la vinculación de objetos de escucha con el atributo onClick para los botones, realiza una llamada a orderCupcake() en startFragment y pasa la cantidad de magdalenas.
<Button
    android:id="@+id/order_one_cupcake"
    android:onClick="@{() -> startFragment.orderCupcake(1)}"
    ... />

<Button
    android:id="@+id/order_six_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(6)}"
    ... />

<Button
    android:id="@+id/order_twelve_cupcakes"
    android:onClick="@{() -> startFragment.orderCupcake(12)}"
    ... />
  1. Ejecuta la app. Observa que los controladores de clics del botón en el fragmento de inicio funcionan de manera correcta.
  2. De manera similar, agrega la variable de datos que se mencionó anteriormente en otros diseños para vincular la instancia de fragmento, fragment_flavor.xml, fragment_pickup.xml y fragment_summary.xml.

En fragment_flavor.xml

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="flavorFragment"
            type="com.example.cupcake.FlavorFragment" />
    </data>

    <ScrollView ...>

En fragment_pickup.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="pickupFragment"
            type="com.example.cupcake.PickupFragment" />
    </data>

    <ScrollView ...>

En fragment_summary.xml:

<layout ...>

    <data>
        <variable
            ... />

        <variable
            name="summaryFragment"
            type="com.example.cupcake.SummaryFragment" />
    </data>

    <ScrollView ...>
  1. En el resto de las clases de fragmentos, en los métodos onViewCreated(), borra el código con el que se configura manualmente el objeto de escucha de clics en los botones.
  2. En los métodos onViewCreated(), vincula la variable de datos del fragmento con la instancia del fragmento. Aquí, usarás la palabra clave this de manera diferente, ya que, dentro del bloque binding?.apply, la palabra clave this hace referencia a la instancia de vinculación en lugar de a la instancia del fragmento. Usa @ y especifica de manera explícita el nombre de la clase del fragmento, por ejemplo, this@FlavorFragment. Los métodos onViewCreated() completados deben verse de la siguiente manera:

El método onViewCreated() en la clase FlavorFragment debe verse de la siguiente manera:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding?.apply {
        lifecycleOwner = viewLifecycleOwner
        viewModel = sharedViewModel
        flavorFragment = this@FlavorFragment
    }
}

El método onViewCreated() en la clase PickupFragment debe verse de la siguiente manera:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       pickupFragment = this@PickupFragment
   }
}

El método onViewCreated() resultante en el método de la clase SummaryFragment debe verse de la siguiente manera:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding?.apply {
       lifecycleOwner = viewLifecycleOwner
       viewModel = sharedViewModel
       summaryFragment = this@SummaryFragment
   }
}
  1. De manera similar, en los demás archivos de diseño, agrega expresiones de vinculación de objetos de escucha con el atributo onClick para los botones.

En fragment_flavor.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> flavorFragment.goToNextScreen()}"
    ... />

En fragment_pickup.xml:

<Button
    android:id="@+id/next_button"
    android:onClick="@{() -> pickupFragment.goToNextScreen()}"
    ... />

En fragment_summary.xml:

<Button
    android:id="@+id/send_button"
    android:onClick="@{() -> summaryFragment.sendOrder()}"
    ...>
  1. Ejecuta la app para verificar que los botones todavía funcionen como se espera. No debería haber cambios visibles en el comportamiento, pero ahora usaste vinculaciones de objetos de escucha para configurar los objetos de escucha de clics.

Felicitaciones por completar este codelab y crear la app Cupcake. Sin embargo, la app todavía no está lista. En el siguiente codelab, agregarás el botón Cancel y modificarás la pila de actividades. Además, aprenderás qué es una pila de actividades y otros temas nuevos. ¡Nos vemos!

10. 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. Usa la rama viewmodel para extraer o descargar el código.

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.

11. Resumen

  • El ViewModel participa de los componentes de la arquitectura de Android, y los datos de app que se guardan dentro de ViewModel se retienen durante los cambios de configuración. Para agregar un elemento ViewModel a tu app, crea una clase nueva y extiéndela desde la clase ViewModel.
  • Se usa el elemento ViewModel compartido para guardar los datos de la app a partir de varios fragmentos en un solo elemento ViewModel. Varios fragmentos en la app accederán al elemento ViewModel compartido con su alcance de actividad.
  • LifecycleOwner es una clase que tiene un ciclo de vida de Android, por ejemplo, una actividad o un fragmento.
  • El observador LiveData ve los cambios en los datos de la app solamente si el propietario del ciclo de vida está en estado activo (STARTED o RESUMED).
  • Las vinculaciones de objetos de escucha son expresiones lambda que se ejecutan cuando se produce un evento, por ejemplo, un evento onClick. Son similares a las referencias de métodos, como textview.setOnClickListener(clickListener), pero las vinculaciones de objetos de escucha te permiten ejecutar expresiones arbitrarias de vinculación de datos.
  • Los métodos de transformación LiveData brindan una manera de realizar manipulaciones de datos en la fuente LiveData y mostrar un objeto LiveData resultante.
  • Los frameworks de Android brindan una clase denominada SimpleDateFormat, que es una clase para formatear y analizar fechas de una manera que tiene en cuenta la configuración regional. Permite dar formato a las fechas (fecha → texto) y analizar (texto → fecha) las fechas.

12. Más información