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 en secuencia. Todos los codelabs del curso se detallan en la página de destino de codelabs sobre los aspectos básicos de Kotlin para Android.

Introducción

En los codelabs anteriores de esta lección, se mejoró el código para la app de GuessTheWord. La app ahora usa objetos ViewModel, de modo 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 se notifican automáticamente cuando cambian los datos observados.

En este codelab, seguirás trabajando con la app de GuessTheWord. Vincularás las vistas con 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 han comunicado indirectamente con la ViewModel, a través de los fragmentos de la app). Una vez que integras 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 usar LiveData como la fuente de vinculación de datos a fin de notificar a la IU sobre cambios en los datos, sin usar los 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 las actividades y los fragmentos
  • Cómo usar objetos ViewModel en tu app
  • Cómo almacenar datos mediante 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 vinculaciones de objetos de escucha para reemplazar los objetos de escucha de clics en un fragmento
  • Cómo agregar formato de strings a expresiones de vinculación de datos

Actividades

  • Las vistas de los diseños de GuessTheWord se comunican indirectamente con los objetos ViewModel, mediante controladores de IU (fragmentos) para retransmitir información. En este codelab, vinculas las vistas de la app con los objetos ViewModel para que estas se comuniquen directamente con los objetos ViewModel.
  • Cambia la app para usar LiveData como la fuente de vinculación de datos. Después de este cambio, los objetos LiveData notifican a la IU sobre los cambios en los datos y ya no se necesitan los métodos del observador de LiveData.

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

El primer jugador mira las palabras en la app y actúa a su vez de manera individual, 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, como "guitarra", como se muestra en la siguiente captura de pantalla.

El primer jugador representa la palabra y tiene cuidado de no decirla realmente.

  • Cuando el segundo jugador adivina la palabra correctamente, el primero presiona el botón Entendido, lo que aumenta el recuento en uno y muestra la palabra siguiente.
  • Si el segundo jugador no puede adivinar la palabra, el primer jugador presiona el botón Omitir, lo que disminuye la cantidad de palabras y pasa a la siguiente palabra.
  • Para finalizar el juego, presiona el botón Finalizar juego. (Esta función no se encuentra en el código de inicio para el 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 de ViewModel, y te permite simplificar tu código mediante LiveData.

Pantalla de título

Pantalla del juego

Pantalla de puntuación

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

  1. Si no usas el código del codelab anterior, descarga el código de inicio para este codelab (opcional). Descomprime el código y abre el proyecto en Android Studio.
  2. Ejecuta la app y comienza a jugar.
  3. Ten en cuenta que el botón Entendido muestra la palabra siguiente y aumenta la puntuación en uno, mientras que el botón Omitir muestra la palabra siguiente y disminuye la puntuación en uno. El botón End Game finaliza el juego.
  4. Desplázate por 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 manera de acceder a las vistas de manera segura en función de la app de GuessTheWord. Sin embargo, el verdadero poder de la vinculación de datos recae en lo que el nombre sugiere: la vinculación de datos directamente con 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 estas se guardan en objetos ViewModel. Entre cada vista y su ViewModel correspondiente, hay un controlador de IU que actúa como una retransmisión entre ellas.

Por ejemplo:

  • El botón Got It (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 el GameViewModel.

La vista Button y GameViewModel no se comunican directamente; necesitan el objeto de escucha de clics en GameFragment.

ViewModel pasado 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. Al pasar 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 configurarás vinculaciones de objetos de escucha para controlar eventos de clic.

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

En este paso, asocias 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 controlar 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 configura en la vista. Cuando sucede el evento escuchado, el objeto de escucha evalúa la expresión lambda. Las vinculaciones de 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, reemplazas 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 del objeto 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 con el método onGameFinish() en GameViewModel.
<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />
  1. En GameFragment, quita las sentencias que establecen los objetos de escucha de clics y las funciones a las que llaman los objetos de escucha de clics. Ya no los necesitas.

Código para 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, asocias 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 objetos 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 el elemento 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 playAgainButton. Si Android Studio muestra un error, limpia y vuelve a compilar el proyecto.

Código para quitar:

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Ejecuta la app, que 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 del botón en ScoreFragment.

Solución de 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 compilar la app, por lo que no ves advertencias ni código rojo mientras escribes el código. Sin embargo, en el tiempo de compilación, se producen errores criptográficos 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, significa que hay un error con la vinculación de datos.
  2. En el archivo en formato XML de diseño, comprueba si hay errores en los atributos onClick que usan la 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, revisa la ortografía de la variable de vinculación de datos.

Por ejemplo, ten en cuenta el error de ortografía del nombre de la función onCorrect() en el siguiente valor de atributo:

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

Observa también el error ortográfico de gameViewModel en la sección <data> del archivo XML:

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

Android Studio no detectará errores como estos hasta que compiles la app y, luego, el compilador mostrará 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 vínculos de datos a los objetos ViewModel, está todo listo para incorporar LiveData.

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

Paso 1: Agrega la palabra LiveData 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.

Configúralo en el objeto LiveData, word desde GameViewModel, mediante la variable de vinculación gameViewModel.

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

Ten en cuenta que no tienes que usar word.value. En su lugar, puedes usar el objeto LiveData real. El objeto LiveData muestra el valor actual del word. Si el valor de word es nulo, el objeto LiveData muestra una string 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 de LiveData word.

Código para 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 está actualizando sin un método de observador en el controlador de IU.

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

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

  1. En score_fragment.xml, agrega el atributo android:text a la vista de texto de puntuación. Asigna scoreViewModel.score al atributo text. Debido a que score es un número entero, conviértelo en una string 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 para el objeto score.

Código para 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 string con vinculación de datos

En el diseño, puedes agregar formato de strings junto con la vinculación de datos. En esta tarea, darás formato a la palabra actual para agregar comillas alrededor de ella. También debe darle formato a la string de puntuación para que incluya como prefijo la Puntuación actual, como se muestra en la siguiente imagen.

  1. En string.xml, agrega las siguientes strings, 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 y la puntuación actuales.
<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 strings quote_format. Pasa gameViewModel.word. De esta manera, se pasa la palabra actual como un argumento a la string de formato.
<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />
  1. Formatea la vista de texto score similar a word_text. En game_fragment.xml, agrega el atributo text a la vista de texto score_text. Usa el recurso de strings score_format, que toma un argumento numérico, representado por el marcador de posición %d. Pasa el objeto LiveData, score, como un argumento a esta string 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 para 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 formato 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 ViewModel, sin usar controladores de clics en el fragmento. También usaste objetos LiveData como la fuente de vinculación de datos para notificar automáticamente a la IU sobre cambios en los datos, sin los métodos del observador de LiveData.

Proyecto de Android Studio: GuessTheWord

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

Vinculación de datos de ViewModel

  • Puedes asociar un ViewModel con un diseño mediante 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.

Cómo asociar un ViewModel a 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 del 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 de los controladores de IU por las vinculaciones de objetos de escucha del archivo de diseño.
  • La vinculación de datos crea un objeto de escucha y lo configura 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 el ViewModel. Cuando cambia LiveData en ViewModel, las vistas del diseño se pueden actualizar automáticamente, sin los métodos del observador en los controladores de IU.
android:text="@{gameViewModel.word}"
  • Para hacer que la vinculación de datos de LiveData funcione, configura 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

Formato de strings con vinculación de datos

  • Con la vinculación de datos, puedes dar formato a un recurso de strings con marcadores de posición como %s para strings y %d para números enteros.
  • Para actualizar el atributo text de la vista, pasa el objeto LiveData como argumento de la string 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 tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Responde estas preguntas

Pregunta 1

¿Cuál de las siguientes afirmaciones no es verdadera respecto de 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

Supongamos que tu app incluye este recurso de strings:
<string name="generic_name">Hello %s</string>

¿Cuál de las siguientes opciones es la sintaxis correcta para aplicar formato a la string 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 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 siguiente lección: 5.4: Transformaciones de LiveData

Para ver vínculos a otros codelabs de este curso, consulta la página de destino de codelabs sobre aspectos básicos de Kotlin para Android.