Aspectos básicos de Kotlin para Android 05.3: Vinculación de datos con ViewModel y LiveData

Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.

Introducción

En los codelabs anteriores de esta lección, mejoraste el código de la app de GuessTheWord. Ahora, la app usa objetos ViewModel, por lo que los datos de la app sobreviven a los cambios de configuración del dispositivo, como las rotaciones de pantalla y los cambios en la disponibilidad del teclado. También agregaste LiveData observables, por lo que las vistas reciben notificaciones automáticamente cuando cambian los datos observados.

En este codelab, seguirás trabajando con la app de GuessTheWord. Vincularás vistas a las clases ViewModel de la app para que las vistas de tu diseño se comuniquen directamente con los objetos ViewModel. (Hasta ahora, en tu app, las vistas se comunicaron indirectamente con el ViewModel, a través de los fragmentos de la app). Después de integrar la vinculación de datos con los objetos ViewModel, ya no necesitas controladores de clics en los fragmentos de la app, por lo que los quitas.

También cambiarás la app de GuessTheWord para que use LiveData como fuente de vinculación de datos para notificar a la IU sobre los cambios en los datos, sin usar métodos de observador de LiveData.

Conocimientos que ya deberías tener

  • Cómo crear apps básicas para Android en Kotlin
  • Cómo funcionan los ciclos de vida de actividades y fragmentos
  • Cómo usar objetos ViewModel en tu app
  • Cómo almacenar datos con LiveData en un ViewModel
  • Cómo agregar métodos de observador para observar los cambios en los datos de LiveData

Qué aprenderás

  • Cómo usar elementos de la biblioteca de vinculación de datos
  • Cómo integrar ViewModel con la vinculación de datos
  • Cómo integrar LiveData con la vinculación de datos
  • Cómo usar las vinculaciones de objetos de escucha para reemplazar los objetos de escucha de clics en un fragmento
  • Cómo agregar formato de cadenas a expresiones de vinculación de datos

Actividades

  • Las vistas de los diseños de GuessTheWord se comunican de forma indirecta con los objetos ViewModel, y usan controladores de IU (fragmentos) para transmitir información. En este codelab, vincularás las vistas de la app a objetos ViewModel para que las vistas se comuniquen directamente con los objetos ViewModel.
  • Cambias la app para que use LiveData como fuente de vinculación de datos. Después de este cambio, los objetos LiveData notifican a la IU los cambios en los datos, y los métodos de observador LiveData ya no son necesarios.

En los codelabs de la lección 5, desarrollarás la app de GuessTheWord, comenzando con el código de inicio. GuessTheWord es un juego de adivinanzas para dos jugadores en el que ambos colaboran para obtener la puntuación más alta posible.

El primer jugador mira las palabras en la app y representa cada una por turnos, asegurándose de no mostrarle la palabra al segundo jugador. El segundo jugador intenta adivinar la palabra.

Para jugar, el primer jugador abre la app en el dispositivo y ve una palabra, por ejemplo, "guitarra", como se muestra en la siguiente captura de pantalla.

El primer jugador representa la palabra, teniendo cuidado de no decirla.

  • Cuando el segundo jugador adivina la palabra correctamente, el primer jugador presiona el botón Entendido, lo que aumenta el recuento en uno y muestra la siguiente palabra.
  • Si el segundo jugador no puede adivinar la palabra, el primer jugador presiona el botón Omitir, lo que disminuye el recuento en uno y pasa a la siguiente palabra.
  • Para finalizar el juego, presiona el botón Finalizar juego. (Esta funcionalidad no se encuentra en el código de partida del primer codelab de la serie).

En este codelab, mejorarás la app de GuessTheWord integrando la vinculación de datos con LiveData en objetos ViewModel. Esto automatiza la comunicación entre las vistas del diseño y los objetos ViewModel, y te permite simplificar tu código con LiveData.

Pantalla de título

Pantalla del juego

Pantalla de puntuación

En esta tarea, ubicarás y ejecutarás el código de partida para este codelab. Puedes usar la app de GuessTheWord que compilaste en el codelab anterior como código de partida o descargar una app de partida.

  1. (Opcional) Si no usas el código del codelab anterior, descarga el código de partida para este codelab. Descomprime el código y abre el proyecto en Android Studio.
  2. Ejecuta la app y juega.
  3. Observa que el botón Entendido muestra la siguiente palabra y aumenta la puntuación en uno, mientras que el botón Omitir muestra la siguiente palabra y disminuye la puntuación en uno. El botón End Game finaliza el juego.
  4. Recorre todas las palabras y observa que la app navega automáticamente a la pantalla de puntuación.

En un codelab anterior, usaste la vinculación de datos como una forma de acceder a las vistas en la app de GuessTheWord con seguridad de tipos. Sin embargo, el verdadero poder de la vinculación de datos radica en hacer lo que sugiere su nombre: vincular datos directamente a los objetos de vista en tu app.

Arquitectura actual de la app

En tu app, las vistas se definen en el diseño XML, y los datos de esas vistas se almacenan en objetos ViewModel. Entre cada vista y su ViewModel correspondiente, hay un controlador de IU que actúa como un relé entre ellos.

Por ejemplo:

  • El botón Entendido se define como una vista Button en el archivo de diseño game_fragment.xml.
  • Cuando el usuario presiona el botón Entendido, un objeto de escucha de clics en el fragmento GameFragment llama al objeto de escucha de clics correspondiente en GameViewModel.
  • La puntuación se actualiza en GameViewModel.

La vista Button y el GameViewModel no se comunican directamente, sino que necesitan el objeto de escucha de clics que se encuentra en el GameFragment.

ViewModel que se pasa a la vinculación de datos

Sería más sencillo si las vistas del diseño se comunicaran directamente con los datos de los objetos ViewModel, sin depender de los controladores de IU como intermediarios.

Los objetos ViewModel contienen todos los datos de la IU en la app de GuessTheWord. Si pasas objetos ViewModel a la vinculación de datos, puedes automatizar parte de la comunicación entre las vistas y los objetos ViewModel.

En esta tarea, asociarás las clases GameViewModel y ScoreViewModel con sus diseños XML correspondientes. También configuraste vinculaciones de objetos de escucha para controlar los eventos de clic.

Paso 1: Agrega la vinculación de datos para GameViewModel

En este paso, asociarás GameViewModel con el archivo de diseño correspondiente, game_fragment.xml.

  1. En el archivo game_fragment.xml, agrega una variable de vinculación de datos del tipo GameViewModel. Si tienes errores en Android Studio, limpia y vuelve a compilar el proyecto.
<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...
  1. En el archivo GameFragment, pasa GameViewModel a la vinculación de datos.

    Para ello, asigna viewModel a la variable binding.gameViewModel, que declaraste en el paso anterior. Coloca este código dentro de onCreateView(), después de que se inicialice viewModel. Si tienes errores en Android Studio, limpia y vuelve a compilar el proyecto.
// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

Paso 2: Usa vinculaciones de objetos de escucha para el control de eventos

Las vinculaciones de objetos de escucha son expresiones de vinculación que se ejecutan cuando se activan eventos como onClick(), onZoomIn() o onZoomOut(). Las vinculaciones de los objetos de escucha se escriben como expresiones lambda.

La vinculación de datos crea un objeto de escucha y lo establece en la vista. Cuando se produce el evento que se escucha, el objeto de escucha evalúa la expresión lambda. Las vinculaciones de los objetos de escucha funcionan con la versión 2.0 o posterior del complemento de Android para Gradle. Para obtener más información, consulta Diseños y expresiones de vinculación.

En este paso, reemplazarás los objetos de escucha de clics en GameFragment por vinculaciones de objetos de escucha en el archivo game_fragment.xml.

  1. En game_fragment.xml, agrega el atributo onClick a skip_button. Define una expresión de vinculación y llama al método onSkip() en GameViewModel. Esta expresión de vinculación se denomina vinculación de escucha.
<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />
  1. De manera similar, vincula el evento de clic de correct_button al método onCorrect() en GameViewModel.
<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onCorrect()}"
   ... />
  1. Vincula el evento de clic de end_game_button al método onGameFinish() en GameViewModel.
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. En GameFragment, quita las instrucciones que establecen los objetos de escucha de clics y las funciones que llaman los objetos de escucha de clics. Ya no los necesitas.

Código que debes quitar:

binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }

/** Methods for buttons presses **/
private fun onSkip() {
   viewModel.onSkip()
}
private fun onCorrect() {
   viewModel.onCorrect()
}
private fun onEndGame() {
   gameFinished()
}

Paso 3: Agrega la vinculación de datos para ScoreViewModel

En este paso, asociarás ScoreViewModel con el archivo de diseño correspondiente, score_fragment.xml.

  1. En el archivo score_fragment.xml, agrega una variable de vinculación del tipo ScoreViewModel. Este paso es similar al que usaste anteriormente para GameViewModel.
<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout
  1. En score_fragment.xml, agrega el atributo onClick a play_again_button. Define una vinculación de objeto de escucha y llama al método onPlayAgain() en ScoreViewModel.
<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />
  1. En ScoreFragment, dentro de onCreateView(), inicializa viewModel. Luego, inicializa la variable de vinculación binding.scoreViewModel.
viewModel = ...
binding.scoreViewModel = viewModel
  1. En ScoreFragment, quita el código que configura el objeto de escucha de clics para el playAgainButton. Si Android Studio muestra un error, limpia y vuelve a compilar el proyecto.

Código que debes quitar:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Ejecuta la app. La app debería funcionar como antes, pero ahora las vistas de botones se comunican directamente con los objetos ViewModel. Las vistas ya no se comunican a través de los controladores de clics de botones en ScoreFragment.

Soluciona problemas de mensajes de error de vinculación de datos

Cuando una app usa la vinculación de datos, el proceso de compilación genera clases intermedias que se usan para la vinculación de datos. Una app puede tener errores que Android Studio no detecta hasta que intentas compilarla, por lo que no ves advertencias ni código rojo mientras escribes el código. Sin embargo, en tiempo de compilación, obtienes errores crípticos que provienen de las clases intermedias generadas.

Si recibes un mensaje de error críptico, haz lo siguiente:

  1. Observa con atención el mensaje en el panel Build de Android Studio. Si ves una ubicación que termina en databinding, hay un error con la vinculación de datos.
  2. En el archivo XML de diseño, busca errores en los atributos onClick que usan vinculación de datos. Busca la función a la que llama la expresión lambda y asegúrate de que exista.
  3. En la sección <data> del XML, verifica la ortografía de la variable de vinculación de datos.

Por ejemplo, observa el error ortográfico en el nombre de la función onCorrect() en el siguiente valor del atributo:

android:onClick="@{() -> gameViewModel.onCorrectx()}"

También ten en cuenta la falta de ortografía de gameViewModel en la sección <data> del archivo en formato XML:

<data>
   <variable
       name="gameViewModelx"
       type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>

Android Studio no detecta errores como estos hasta que compilas la app. Luego, el compilador muestra un mensaje de error como el siguiente:

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding

La vinculación de datos funciona bien con LiveData que se usa con objetos ViewModel. Ahora que agregaste la vinculación de datos a los objetos ViewModel, puedes incorporar LiveData.

En esta tarea, cambiarás la app de GuessTheWord para que use LiveData como fuente de vinculación de datos para notificar a la IU sobre los cambios en los datos, sin usar los métodos de observador de LiveData.

Paso 1: Agrega LiveData de palabras al archivo game_fragment.xml

En este paso, vinculas la vista de texto de la palabra actual directamente al objeto LiveData en ViewModel.

  1. En game_fragment.xml, agrega el atributo android:text a la vista de texto word_text.

Establécelo en el objeto LiveData, word del GameViewModel, con la variable de vinculación, gameViewModel.

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

Ten en cuenta que no es necesario que uses word.value. En su lugar, puedes usar el objeto LiveData real. El objeto LiveData muestra el valor actual de word. Si el valor de word es nulo, el objeto LiveData muestra una cadena vacía.

  1. En GameFragment, en onCreateView(), después de inicializar gameViewModel, establece la actividad actual como el propietario del ciclo de vida de la variable binding. Esto define el alcance del objeto LiveData anterior, lo que permite que el objeto actualice automáticamente las vistas en el diseño, game_fragment.xml.
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. En GameFragment, quita el observador para el LiveData word.

Código que debes quitar:

/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
   binding.wordText.text = newWord
})
  1. Ejecuta la app y juega. Ahora, la palabra actual se actualiza sin un método de observador en el controlador de IU.

Paso 2: Agrega LiveData de la puntuación al archivo score_fragment.xml

En este paso, vincularás el LiveData score a la vista de texto de la puntuación en el fragmento de puntuación.

  1. En score_fragment.xml, agrega el atributo android:text a la vista de texto de la puntuación. Asigna scoreViewModel.score al atributo text. Como score es un número entero, conviértelo en una cadena con String.valueOf().
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />
  1. En ScoreFragment, después de inicializar scoreViewModel, establece la actividad actual como el propietario del ciclo de vida de la variable binding.
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
  1. En ScoreFragment, quita el observador del objeto score.

Código que debes quitar:

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Ejecuta la app y juega. Observa que la puntuación en el fragmento de puntuación se muestra correctamente, sin un observador en el fragmento de puntuación.

Paso 3: Agrega formato de cadenas con vinculación de datos

En el diseño, puedes agregar formato de cadenas junto con la vinculación de datos. En esta tarea, formatearás la palabra actual para agregarle comillas. También le das formato a la cadena de puntuación para agregarle el prefijo Puntuación actual, como se muestra en la siguiente imagen.

  1. En string.xml, agrega las siguientes cadenas, que usarás para dar formato a las vistas de texto word y score. %s y %d son los marcadores de posición para la palabra actual y la puntuación actual.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
  1. En game_fragment.xml, actualiza el atributo text de la vista de texto word_text para usar el recurso de cadena quote_format. Pasa gameViewModel.word. Esto pasa la palabra actual como un argumento a la cadena de formato.
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. Aplica el formato a la vista de texto score de forma similar a word_text. En game_fragment.xml, agrega el atributo text a la vista de texto score_text. Usa el recurso de cadena score_format, que toma un argumento numérico, representado por el marcador de posición %d. Pasa el objeto LiveData, score, como argumento a esta cadena de formato.
<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />
  1. En la clase GameFragment, dentro del método onCreateView(), quita el código del observador score.

Código que debes quitar:

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Limpia, vuelve a compilar y ejecuta tu app, y luego juega. Observa que la palabra actual y la puntuación tienen el formato adecuado en la pantalla del juego.

¡Felicitaciones! Integraste LiveData y ViewModel con la vinculación de datos en tu app. Esto permite que las vistas de tu diseño se comuniquen directamente con el ViewModel, sin usar controladores de clics en el fragmento. También usaste objetos LiveData como fuente de vinculación de datos para notificar automáticamente a la IU sobre cambios en los datos, sin los métodos de observador de LiveData.

Proyecto de Android Studio: GuessTheWord

  • La biblioteca de vinculación de datos funciona a la perfección con los componentes de arquitectura de Android, como ViewModel y LiveData.
  • Los diseños de tu app se pueden vincular a los datos de los componentes de arquitectura, que ya te ayudan a administrar el ciclo de vida del controlador de la IU y a notificar cambios en los datos.

Vinculación de datos de ViewModel

  • Puedes asociar un ViewModel con un diseño usando la vinculación de datos.
  • Los objetos ViewModel contienen los datos de la IU. Si pasas objetos ViewModel a la vinculación de datos, puedes automatizar parte de la comunicación entre las vistas y los objetos ViewModel.

Sigue estos pasos para asociar un ViewModel con un diseño:

  • En el archivo de diseño, agrega una variable de vinculación de datos del tipo ViewModel.
   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  • En el archivo GameFragment, pasa GameViewModel a la vinculación de datos.
binding.gameViewModel = viewModel

Vinculaciones de objetos de escucha

  • Las vinculaciones de objetos de escucha son expresiones de vinculación en el diseño que se ejecutan cuando se activan eventos de clic, como onClick().
  • Las vinculaciones de los objetos de escucha se escriben como expresiones lambda.
  • Con las vinculaciones de objetos de escucha, reemplazas los objetos de escucha de clics en los controladores de la IU por vinculaciones de objetos de escucha en el archivo de diseño.
  • La vinculación de datos crea un objeto de escucha y lo establece en la vista.
 android:onClick="@{() -> gameViewModel.onSkip()}"

Cómo agregar LiveData a la vinculación de datos

  • Los objetos LiveData se pueden usar como fuente de vinculación de datos para notificar automáticamente a la IU sobre cambios en los datos.
  • Puedes vincular la vista directamente al objeto LiveData en ViewModel. Cuando cambia el LiveData en el ViewModel, las vistas del diseño se pueden actualizar automáticamente, sin los métodos de observador en los controladores de la IU.
android:text="@{gameViewModel.word}"
  • Para que funcione la vinculación de datos de LiveData, establece la actividad actual (el controlador de IU) como el propietario del ciclo de vida de la variable binding en el controlador de IU.
binding.lifecycleOwner = this

Formateo de cadenas con vinculación de datos

  • Con la vinculación de datos, puedes dar formato a un recurso de cadena con marcadores de posición como %s para cadenas y %d para números enteros.
  • Para actualizar el atributo text de la vista, pasa el objeto LiveData como argumento a la cadena de formato.
 android:text="@{@string/quote_format(gameViewModel.word)}"

Curso de Udacity:

Documentación para desarrolladores de Android:

En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.

Responde estas preguntas:

Pregunta 1

¿Cuál de las siguientes afirmaciones no es verdadera sobre las vinculaciones de objetos de escucha?

  • Las vinculaciones de los objetos de escucha son expresiones de vinculación que se ejecutan cuando ocurre un evento.
  • Las vinculaciones de los objetos de escucha funcionan con todas las versiones del complemento de Android para Gradle.
  • Las vinculaciones de los objetos de escucha se escriben como expresiones lambda.
  • Las vinculaciones de objetos de escucha son similares a las referencias de métodos, pero te permiten ejecutar expresiones de vinculación de datos arbitrarias.

Pregunta 2

Supón que tu app incluye este recurso de cadena:
<string name="generic_name">Hello %s</string>

¿Cuál de las siguientes opciones es la sintaxis correcta para aplicar formato a la cadena con la expresión de vinculación de datos?

  • android:text= "@{@string/generic_name(user.name)}"
  • android:text= "@{string/generic_name(user.name)}"
  • android:text= "@{@generic_name(user.name)}"
  • android:text= "@{@string/generic_name,user.name}"

Pregunta 3

¿Cuándo se evalúa y ejecuta una expresión que vincula un objeto de escucha?

  • Cuando se modifican los datos que retiene el LiveData
  • Cuando se vuelve a crear una actividad con un cambio de configuración
  • Cuando se produce un evento como onClick()
  • Cuando la actividad pasa a segundo plano

Comienza la próxima lección: 5.4: Transformaciones de LiveData

Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.