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 elementoViewModel
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.
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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
Explicación del código de inicio
- 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. - 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.
- 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
ySummaryFragment.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
ysummaryFragment
) 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
- 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ó.
- 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.
- Conecta los destinos de fragmentos en el gráfico de navegación. Crea una acción de
startFragment
aflavorFragment
, una conexión deflavorFragment
apickupFragment
y una conexión depickupFragment
asummaryFragment
. Si necesitas instrucciones más detalladas, sigue los pasos que se indican a continuación. - 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.
- 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.
- 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.
- Las tres acciones nuevas que creaste también deben reflejarse en el panel Component Tree.
- 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.
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.
Cómo navegar del fragmento de inicio al fragmento de sabor
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.
- En la ventana Project, abre el archivo Kotlin app > java > com.example.cupcake > StartFragment.
- 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étodoorderCupcake()
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) }
- 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 elementoNavController
con el métodofindNavController()
y, para llamar anavigate()
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 tunav_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)
}
- Agrega la importación
import
androidx.navigation.fragment.findNavController
o elige entre las opciones que ofrece Android Studio.
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.
- 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. - En
FlavorFragment.kt
, dentro del métodogoToNextScreen()
, 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 tunav_graph.xml.
fun goToNextScreen() {
findNavController().navigate(R.id.action_flavorFragment_to_pickupFragment)
}
Recuerda el elemento import androidx.navigation.fragment.findNavController
.
- De manera similar, en
PickupFragment.kt
, dentro del métodogoToNextScreen()
, 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
.
- 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.
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 (←).
- En
MainActivity.kt
, anula el métodoonCreate()
para configurar el controlador de navegación. Obtén una instancia del elementoNavController
desde el objetoNavHostFragment
. - Realiza una llamada a
setupActionBarWithNavController(navController)
y pasa la instancia deNavController
. 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)
}
}
- Agrega las importaciones necesarias cuando Android Studio lo solicite.
import android.os.Bundle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupActionBarWithNavController
- 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. - En
nav_graph.xml
, modifica el atributoandroid: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>
- 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.
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.
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.
- En la ventana Project de Android Studio, haz clic con el botón derecho en com.example.cupcake > New > Package.
- Se abrirá un diálogo New Package. Asígnale al paquete el nombre
com.example.cupcake.model
.
- Crea la clase Kotlin
OrderViewModel
en el paquetemodel
. En la ventana Project, haz clic con el botón derecho en el paquetemodel
y selecciona New > Kotlin File/Class. En el diálogo nuevo, asigna el nombre de archivoOrderViewModel
.
- En
OrderViewModel.kt
, cambia la firma de clase para extender desdeViewModel
.
import androidx.lifecycle.ViewModel
class OrderViewModel : ViewModel() {
}
- Dentro de la clase
OrderViewModel
, agrega las propiedades que se mencionaron anteriormente comoprivate
val
. - 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
- 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. - 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 necesitaprivate
ni otro modificador de visibilidad antes de la palabra clavefun
). El modificador predeterminado de visibilidad de Kotlin espublic
.
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
}
fun setFlavor(desiredFlavor: String) {
_flavor.value = desiredFlavor
}
fun setDate(pickupDate: String) {
_date.value = pickupDate
}
- 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 instanciaViewModel
con alcance para el fragmento actual. Será diferente para los distintos fragmentos.activityViewModels()
te brinda la instanciaViewModel
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>()
- En la clase
StartFragment
, obtén una referencia al modelo de vista compartido como una variable de clase. Usa el delegado de propiedadesby activityViewModels()
de Kotlin de la bibliotecafragment-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
- Repite el paso anterior para las clases
FlavorFragment
,PickupFragment
ySummaryFragment
. Usarás esta instanciasharedViewModel
en secciones posteriores del codelab. - Si regresas a la clase
StartFragment
, ahora puedes usar el modelo de vista. Al principio del métodoorderCupcake()
, llama al métodosetQuantity()
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)
}
- 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 claseStartFragment
en un paso posterior.
fun hasNoFlavorSet(): Boolean {
return _flavor.value.isNullOrEmpty()
}
- En la clase
StartFragment
, dentro del métodoorderCupcake()
, 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)
}
- 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
- En
layout/fragment_flavor.xml
, agrega una etiqueta<data>
dentro de la etiqueta<layout>
raíz. Agrega una variable de diseño llamadaviewModel
del tipocom.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 ...>
...
- De manera similar, repite el paso anterior para
fragment_pickup.xml
yfragment_summary.xml
a fin de agregar la variable de diseñoviewModel
. Usarás esta variable en secciones posteriores. No es necesario que agregues este código enfragment_start.xml
, ya que este diseño no usa el modelo de vista compartido. - En la clase
FlavorFragment
, dentro deonViewCreated()
, 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 bloquebinding?.
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
- Repite el mismo paso para el método
onViewCreated()
dentro de las clasesPickupFragment
ySummaryFragment
.
binding?.apply {
viewModel = sharedViewModel
...
}
- En
fragment_flavor.xml
, usa la variable nueva de diseño,viewModel
, para configurar el atributochecked
de los botones de selección que se basan en el valorflavor
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 VanillaRadioButton
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.
- 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étodoviewModel
.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>
- Ejecuta la app y observa cómo se selecciona la opción Vanilla de forma predeterminada en el fragmento de sabor.
¡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.
- En la clase
OrderViewModel
, agrega la siguiente función denominadagetPickupOptions()
para crear y mostrar la lista de fechas de retiro. Dentro del método, crea una variableval
llamadaoptions
y, luego, inicialízala enmutableListOf
<String>()
.
private fun getPickupOptions(): List<String> {
val options = mutableListOf<String>()
}
- 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.
- Obtén una instancia
Calendar
y asígnala a una variable nueva. Configúrala comoval
. Esta variable contendrá la fecha y la hora actuales. También importajava.util.Calendar
.
val calendar = Calendar.getInstance()
- 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)
}
- 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
}
- En la clase
OrderViewModel
, agrega una propiedad de clase llamadadateOptions
que se haya configurado comoval
. Inicialízala con el métodogetPickupOptions()
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)
- En
fragment_pickup.xml
, para el botón de selecciónoption0
, usa la variable nueva de diseño,viewModel
, a fin de configurar el atributochecked
según el valordate
en el modelo de vista. Compara la propiedadviewModel.date
con la primera string en la listadateOptions
, que representa la fecha actual. Usa la funciónequals
para comparar; la expresión de vinculación final se verá de la siguiente manera:
@{viewModel.date.equals(viewModel.dateOptions[0])}
- 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 asetDate()
enviewModel
y pasadateOptions[0]
. - Para el mismo botón de selección, configura el valor del atributo
text
en la primera string de la listadateOptions
.
<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]}"
...
/>
- 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]}"
... />
- 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.
- Dentro de la clase
OrderViewModel
, crea una función llamadaresetOrder()
para restablecer las propiedadesMutableLiveData
en el modelo de vista. Asigna el valor de la fecha actual de la listadateOptions
a_date.
value.
fun resetOrder() {
_quantity.value = 0
_flavor.value = ""
_date.value = dateOptions[0]
_price.value = 0.0
}
- Agrega un bloque
init
a la clase y llama al método nuevoresetOrder()
desde este.
init {
resetOrder()
}
- 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 deOrderViewModel
.
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
- Vuelve a ejecutar la app y observa que la fecha actual se seleccionó de forma predeterminada.
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.
- 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 ...>
...
- En
SummaryFragment
, enonViewCreated()
, asegúrate de que se inicialicebinding.viewModel
. - En
fragment_summary.xml
, lee desde el modelo de vista para actualizar la pantalla con los detalles del resumen del pedido. Actualiza el elementoTextViews
de la cantidad, el sabor y la fecha. Para ello, agrega los siguientes atributos de texto. La cantidad es del tipoInt
, 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}"
... />
- Ejecuta y prueba la app para verificar que las opciones del pedido que seleccionaste se muestren en el resumen del pedido.
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.
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.
- 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 modificadorconst
y, para configurarlo como de solo lectura, usaval
.
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.
- 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
).
- 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ónsetQuantity()
.
fun setQuantity(numberCupcakes: Int) {
_quantity.value = numberCupcakes
updatePrice()
}
Cómo vincular la propiedad de precio con la IU
- En los diseños para
fragment_flavor.xml
,fragment_pickup.xml
yfragment_summary.xml
, asegúrate de que se defina la variable de datosviewModel
del tipocom.example.cupcake.model.OrderViewModel
.
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
</data>
<ScrollView ...>
...
- 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
...
}
- 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 archivofragment_flavor.xml
. Para la vista de textosubtotal
, configura el valor del atributoandroid: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>
- 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.
- Ahora, realiza un cambio similar para los fragmentos de retiro y resumen. En los diseños
fragment_pickup.xml
yfragment_summary.xml
, modifica las vistas de texto para usar también la propiedadviewModel
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)}"
... />
…
- 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.
- 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
- 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 listadateOptions
, que siempre es el día actual.
private fun updatePrice() {
_price.value = (quantity.value ?: 0) * PRICE_PER_CUPCAKE
if (dateOptions[0] == _date.value) {
}
}
- 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
}
- Llama al método auxiliar
updatePrice()
desdesetDate()
para agregar los cargos por retiro en el mismo día.
fun setDate(pickupDate: String) {
_date.value = pickupDate
updatePrice()
}
- 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.
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.
- En las clases
FlavorFragment
,PickupFragment
ySummaryFragment
, dentro del métodoonViewCreated()
, agrega lo siguiente en el bloquebinding?.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 objetosLiveData
.
binding?.apply {
lifecycleOwner = viewLifecycleOwner
...
}
- 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.
- 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.
- 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.
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>
).
- En la clase
OrderViewModel
, cambia el tipo de propiedad de copia de seguridad aLiveData<String>
en lugar deLiveData<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>
- Usa
Transformations.map()
para inicializar la variable nueva, pasa el elemento_price
y una función lambda. Usa el métodogetCurrencyInstance()
en la claseNumberFormat
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
.
- 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!
- 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.
- En el archivo de diseño
fragment_start.xml
, agrega una variable de datos llamadastartFragment
del tipocom.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 ...>
- En
StartFragment.kt
, en el métodoonViewCreated()
, vincula la variable nueva de datos con la instancia del fragmento. Puedes acceder a la instancia del fragmento dentro del fragmento con la palabra clavethis
. Quita el bloquebinding?.
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
}
- En
fragment_start.xml
, agrega objetos de escucha de eventos mediante la vinculación de objetos de escucha con el atributoonClick
para los botones, realiza una llamada aorderCupcake()
enstartFragment
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)}"
... />
- Ejecuta la app. Observa que los controladores de clics del botón en el fragmento de inicio funcionan de manera correcta.
- 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
yfragment_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 ...>
- 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. - En los métodos
onViewCreated()
, vincula la variable de datos del fragmento con la instancia del fragmento. Aquí, usarás la palabra clavethis
de manera diferente, ya que, dentro del bloquebinding?.apply
, la palabra clavethis
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étodosonViewCreated()
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
}
}
- 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()}"
...>
- 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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- En el cuadro de diálogo, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.
Abre el proyecto en Android Studio
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se configuró la app.
11. Resumen
- El
ViewModel
participa de los componentes de la arquitectura de Android, y los datos de app que se guardan dentro deViewModel
se retienen durante los cambios de configuración. Para agregar un elementoViewModel
a tu app, crea una clase nueva y extiéndela desde la claseViewModel
. - Se usa el elemento
ViewModel
compartido para guardar los datos de la app a partir de varios fragmentos en un solo elementoViewModel
. Varios fragmentos en la app accederán al elementoViewModel
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
oRESUMED
). - 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, comotextview.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 fuenteLiveData
y mostrar un objetoLiveData
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.