Aspectos básicos de Kotlin para Android 05.1: ViewModel y ViewModelFactory

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.

Pantalla de título

Pantalla del juego

Pantalla de puntuación

Introducción

En este codelab, aprenderás sobre uno de los componentes de la arquitectura de Android, ViewModel:

  • Usas la clase ViewModel a fin de almacenar y administrar datos relacionados con la IU de una manera optimizada para los ciclos de vida. La clase ViewModel permite que se conserven los datos luego de los cambios de configuración del dispositivo, como las rotaciones de pantalla y la disponibilidad del teclado.
  • Usa la clase ViewModelFactory para crear una instancia y mostrar el objeto ViewModel que sobrevive a los cambios de configuración.

Conocimientos que ya deberías tener

  • Cómo crear apps básicas para Android en Kotlin
  • Cómo usar el gráfico de navegación para implementar la navegación en la app
  • Cómo agregar un código para navegar entre los destinos de tu app y pasar datos entre los destinos de navegación
  • Cómo funcionan los ciclos de vida de las actividades y los fragmentos
  • Cómo agregar información de registro a una app y leer registros con Logcat en Android Studio

Qué aprenderás

Actividades

  • Agrega un objeto ViewModel a la app para guardar los datos de app y sobrevivir a los cambios de configuración.
  • Usa ViewModelFactory y el patrón de diseño del método de fábrica para crear una instancia de un objeto ViewModel con parámetros del constructor.

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 esta tarea, descargarás y ejecutarás la app de inicio y examinarás el código.

Paso 1: Comienza

  1. Descarga el código de inicio de GuessTheWord y abre el proyecto en Android Studio.
  2. Ejecuta la app en un dispositivo con Android o en un emulador.
  3. Presiona los botones. Observa que el botón Omitir muestra la palabra siguiente y disminuye la puntuación en uno, y el botón Entendido muestra la siguiente palabra y aumenta la puntuación en una. El botón Finalizar juego no está implementado, por lo que no sucede nada cuando lo presionas.

Paso 2: Realiza una explicación del código

  1. En Android Studio, explora el código para familiarizarte con el funcionamiento de la app.
  2. Asegúrate de revisar los archivos que se describen a continuación, que son particularmente importantes.

MainActivity.kt

Este archivo solo contiene el código predeterminado generado por la plantilla.

res/diseño/principal_actividad.xml

Este archivo contiene el diseño principal de la app. El objeto NavHostFragment aloja los otros fragmentos a medida que el usuario navega por la app.

Fragmentos de la IU

El código de inicio tiene tres fragmentos en tres paquetes diferentes en el paquete com.example.android.guesstheword.screens:

  • title/TitleFragment para la pantalla de título
  • game/GameFragment para la pantalla del juego
  • score/ScoreFragment para la pantalla de puntuación

pantallas/title/TitleFragment.kt

El fragmento de título es la primera pantalla que se muestra cuando se inicia la app. Para navegar a la pantalla del juego, se configura un controlador de clics en el botón Play.

pantallas/juego/GameFragment.kt

Este es el fragmento principal, donde se produce la mayor parte de la acción del juego:

  • Las variables se definen para la palabra actual y la puntuación actual.
  • El wordList definido dentro del método resetList() es una lista de muestra de palabras que se usará en el juego.
  • El método onSkip() es el controlador de clics del botón Skip. Se reduce la puntuación a 1 y, luego, se muestra la palabra siguiente con el método nextWord().
  • El método onCorrect() es el controlador de clics del botón Go. Este método se implementa de manera similar al método onSkip(). La única diferencia es que este método suma 1 a la puntuación en lugar de restarla.

pantallas/score/ScoreFragment.kt

ScoreFragment es la pantalla final del juego y muestra el resultado final del jugador. En este codelab, agregarás la implementación para mostrar esta pantalla y mostrar la puntuación final.

res/navigation/main_navigation.xml

En el gráfico de navegación, se muestra cómo se conectan los fragmentos a través de la navegación:

  • Desde el fragmento de título, el usuario puede navegar al fragmento del juego.
  • Desde el fragmento del juego, el usuario puede navegar al fragmento de la puntuación.
  • Desde el fragmento de puntuación, el usuario puede regresar al fragmento del juego.

En esta tarea, encontrarás problemas con la app de inicio de GuessTheWord.

  1. Ejecuta el código de inicio y juega con varias palabras; para ello, presiona Omitir o Entendido después de cada palabra.
  2. La pantalla del juego ahora muestra una palabra y la puntuación actual. Cambia la orientación de la pantalla girando el dispositivo o el emulador. Ten en cuenta que se perdió la puntuación actual.
  3. Ejecuta el juego con algunas palabras más. Cuando se muestre la pantalla del juego con alguna puntuación, cierra y vuelve a abrir la app. Ten en cuenta que el juego se reinicia desde el principio, ya que no se guarda el estado de la app.
  4. Juega con algunas palabras y, luego, presiona el botón End Game. Observa que no sucede nada.

Problemas en la app:

  • La app de inicio no guarda ni restablece el estado de la app durante los cambios de configuración, como cuando cambia la orientación del dispositivo o cuando se cierra y se reinicia la app.
    Puedes resolver este problema usando la devolución de llamada onSaveInstanceState(). Sin embargo, para usar el método onSaveInstanceState(), debes escribir código adicional a fin de guardar el estado en un paquete y, luego, implementar la lógica para recuperar ese estado. Además, la cantidad de datos que se pueden almacenar es mínima.
  • La pantalla del juego no navega a la pantalla de puntuación cuando el usuario presiona el botón End Game.

Puedes resolver estos problemas usando los componentes de la arquitectura de apps que aprenderás en este codelab.

Arquitectura de la app

La arquitectura de apps es una forma de diseñar tus apps y clases, y las relaciones entre ellas, de modo que el código esté organizado, funcione bien en situaciones particulares y sea fácil de usar. En este conjunto de cuatro codelabs, las mejoras que realices en la app de GuessTheWord seguirán los lineamientos de la arquitectura de apps para Android, y usarás los componentes de la arquitectura de Android. La arquitectura de la app para Android es similar al patrón de arquitectura MVVM (model-view-viewmodel).

La app de GuessTheWord sigue el principio de diseño de separación de problemas y se divide en clases, y cada clase aborda una inquietud diferente. En este primer codelab de la clase, las clases con las que trabajas son un controlador de IU, una ViewModel y una ViewModelFactory.

Controlador de IU

Un controlador de IU es una clase basada en IU, como Activity o Fragment. Un controlador de IU solo debe contener lógica que se ocupe de interacciones del sistema operativo y de IU, como mostrar vistas y capturar las entradas del usuario. No uses la lógica de toma de decisiones, como la que determina el texto que se mostrará, en el controlador de IU.

En el código de inicio de GuessTheWord, los controladores de IU son los tres fragmentos: GameFragment, ScoreFragment, y TitleFragment. De acuerdo con el principio de separación de problemas, GameFragment solo es responsable de dibujar elementos del juego en la pantalla y saber cuándo el usuario presiona los botones, y nada más. Cuando el usuario presiona un botón, esta información se pasa a la GameViewModel.

ViewModel

Una ViewModel contiene datos que se mostrarán en un fragmento o una actividad asociada con la ViewModel. Un ViewModel puede hacer cálculos y transformaciones simples en los datos a fin de prepararlos para que el controlador de la IU los muestre. En esta arquitectura, ViewModel toma las decisiones.

El GameViewModel contiene datos como el valor de puntuación, la lista de palabras y la palabra actual, ya que estos son los datos que se mostrarán en la pantalla. GameViewModel también contiene la lógica empresarial para realizar cálculos simples a fin de decidir cuál es el estado actual de los datos.

ViewModelFactory

Un objeto ViewModelFactory crea una instancia de objetos ViewModel, con o sin parámetros de constructor.

En codelabs posteriores, aprenderás sobre otros componentes de la arquitectura de Android relacionados con controladores de IU y ViewModel.

La clase ViewModel está diseñada para almacenar y administrar los datos relacionados con la IU. En esta app, cada ViewModel se asocia con un fragmento.

En esta tarea, agregarás tu primer ViewModel a tu app, GameViewModel para GameFragment. También aprenderás lo que significa que ViewModel está optimizado para los ciclos de vida.

Paso 1: Agrega la clase GameViewModel

  1. Abre el archivo build.gradle(module:app). Dentro del bloque dependencies, agrega la dependencia de Gradle para ViewModel.

    Si usas la versión más reciente de la biblioteca, se compilará la app de la solución como se espera. Si no es así, intenta resolver el problema o vuelve a la versión que se muestra a continuación.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. En la carpeta screens/game/ del paquete, crea una nueva clase de Kotlin llamada GameViewModel.
  2. Haz que la clase GameViewModel extienda la clase abstracta ViewModel.
  3. Para ayudarte a comprender mejor cómo ViewModel está optimizado para los ciclos de vida, agrega un bloque init con una sentencia log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Paso 2: Anula onCleared() y agrega registros

El elemento ViewModel se destruye cuando se desconecta el fragmento asociado o cuando finaliza la actividad. Justo antes de que se destruya ViewModel, se llama a la devolución de llamada onCleared() para limpiar los recursos.

  1. En la clase GameViewModel, anula el método onCleared().
  2. Agrega una instrucción de registro dentro de onCleared() para hacer un seguimiento del ciclo de vida de GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Paso 3: Asocia GameViewModel con el fragmento de juego

Se debe asociar un ViewModel con un controlador de IU. Para asociar ambos, crea una referencia al ViewModel dentro del controlador de IU.

En este paso, crearás una referencia de GameViewModel dentro del controlador de IU correspondiente, que es GameFragment.

  1. En la clase GameFragment, agrega un campo del tipo GameViewModel en el nivel superior como una variable de clase.
private lateinit var viewModel: GameViewModel

Paso 4: Inicializa el ViewModel

Durante los cambios de configuración, como las rotaciones de pantalla, se vuelven a crear los controladores de IU, como los fragmentos. Sin embargo, ViewModel instancias sobreviven. Si creas la instancia ViewModel con la clase ViewModel, se creará un objeto nuevo cada vez que se vuelva a crear el fragmento. En su lugar, crea la instancia ViewModel con un elemento ViewModelProvider.

Cómo funciona ViewModelProvider:

  • ViewModelProvider muestra una ViewModel existente si existe, o crea una nueva si aún no existe.
  • ViewModelProvider crea una instancia de ViewModel asociada con el alcance determinado (una actividad o un fragmento).
  • Se conservará la ViewModel creada siempre que el alcance esté activo. Por ejemplo, si el alcance es un fragmento, se retiene el ViewModel hasta que se desconecta el fragmento.

Inicializa el ViewModel con el método ViewModelProviders.of() para crear un ViewModelProvider:

  1. En la clase GameFragment, inicializa la variable viewModel. Coloca este código dentro de onCreateView(), después de la definición de la variable de vinculación. Usa el método ViewModelProviders.of() y pasa el contexto GameFragment asociado y la clase GameViewModel.
  2. Arriba de la inicialización del objeto ViewModel, agrega una instrucción de registro para registrar la llamada al método ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Ejecuta la app. En Android Studio, abre el panel Logcat y filtra en Game. Presiona el botón Reproducir en tu dispositivo o emulador. Se abrirá la pantalla del juego.

    Como se muestra en Logcat, el método onCreateView() de GameFragment llama al método ViewModelProviders.of() para crear el GameViewModel. Las instrucciones de registro que agregaste a GameFragment y GameViewModel aparecen en el Logcat.

  1. Habilita la configuración de girar automáticamente en tu dispositivo o emulador y cambia la orientación de la pantalla varias veces. GameFragment se destruye y se vuelve a crear cada vez, por lo que se llama a ViewModelProviders.of() cada vez. Sin embargo, el GameViewModel se crea solo una vez y no se vuelve a crear ni se destruye por cada llamada.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. Sal del juego o sal del fragmento de juego. Se destruye GameFragment. También se destruye el GameViewModel asociado y se llama a la devolución de llamada onCleared().
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel sobrevive a los cambios de configuración, por lo que es un buen lugar para los datos que necesitan sobrevivir a los cambios de configuración:

  • Coloca los datos que se mostrarán en la pantalla y el código para procesar esos datos, en el ViewModel.
  • El elemento ViewModel nunca debe contener referencias a fragmentos, actividades o vistas, ya que estas no sobreviven a los cambios de configuración.

A modo de comparación, aquí se muestra cómo se controlan los datos de IU de GameFragment en la app de inicio antes de agregar ViewModel y después de agregar ViewModel:

  • Antes de agregar ViewModel:
    Cuando la app atraviesa un cambio de configuración, como una rotación de pantalla, el fragmento del juego se destruye y se vuelve a crear. Se pierden los datos.
  • Después de agregar ViewModel y mover los datos de la IU del fragmento del juego a ViewModel:
    Todos los datos que el fragmento debe mostrar ahora son ViewModel. Cuando la app pasa por un cambio de configuración, el ViewModel permanece vigente y se conservan los datos.

En esta tarea, moverás los datos de la IU de la app a la clase GameViewModel y los métodos para procesar los datos. Esto sirve para conservar los datos durante los cambios de configuración.

Paso 1: Mueve los campos de datos y el procesamiento de datos al ViewModel

Mueve los siguientes campos de datos y métodos de GameFragment a GameViewModel:

  1. Mueve los campos de datos word, score y wordList. Asegúrate de que word y score no sean private.

    No muevas la variable de vinculación GameFragmentBinding, ya que contiene referencias a las vistas. Esta variable se usa para aumentar el diseño, configurar los objetos de escucha de clics y mostrar los datos en la pantalla (responsabilidades del fragmento).
  2. Mueve los métodos resetList() y nextWord(). Estos métodos deciden qué palabra mostrar en pantalla.
  3. Desde el método onCreateView(), mueve las llamadas de método a resetList() y nextWord() al bloque init de GameViewModel.

    Estos métodos deben estar en el bloque init, ya que debes restablecer la lista de palabras cuando se crea el ViewModel, no cada vez que se crea el fragmento. Puedes borrar la instrucción de registro en el bloque init de GameFragment.

Los controladores de clics onSkip() y onCorrect() en GameFragment contienen código para procesar los datos y actualizar la IU. El código para actualizar la IU debe permanecer en el fragmento, pero el código para procesar los datos se debe mover a ViewModel.

Por ahora, coloca los métodos idénticos en ambos lugares:

  1. Copia los métodos onSkip() y onCorrect() de GameFragment a GameViewModel.
  2. En GameViewModel, asegúrate de que los métodos onSkip() y onCorrect() no sean private, ya que harás referencia a estos métodos desde el fragmento.

Este es el código para la clase GameViewModel, después de refactorizar:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

Este es el código para la clase GameFragment, después de refactorizar:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

Paso 2: Actualiza las referencias a los controladores de clics y los campos de datos en GameFragment

  1. En GameFragment, actualiza los métodos onSkip() y onCorrect(). Quita el código para actualizar la puntuación y, en su lugar, llama a los métodos onSkip() y onCorrect() correspondientes en viewModel.
  2. Como moviste el método nextWord() a ViewModel, el fragmento del juego ya no puede acceder a él.

    En GameFragment, en los métodos onSkip() y onCorrect(), reemplaza la llamada a nextWord() por updateScoreText() y updateWordText(). Estos métodos muestran los datos en la pantalla.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. En GameFragment, actualiza las variables score y word para usar las variables GameViewModel, que ahora están en GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. En GameViewModel, dentro del método nextWord(), quita las llamadas a los métodos updateWordText() y updateScoreText(). Ahora se llama a estos métodos desde el GameFragment.
  2. Compila la app y asegúrate de que no haya errores. Si tienes errores, limpia y vuelve a compilar el proyecto.
  3. Ejecuta la app y juega con algunas palabras. En la pantalla del juego, rota el dispositivo. Observa que la puntuación actual y la palabra actual se conservan después del cambio de orientación.

Buen trabajo. Ahora todos tus datos se almacenan en una ViewModel, por lo que se conservan durante los cambios de configuración.

En esta tarea, implementarás el objeto de escucha de clics del botón End Game.

  1. En GameFragment, agrega un método llamado onEndGame(). Se llamará al método onEndGame() cuando el usuario presione el botón End Game.
private fun onEndGame() {
   }
  1. En GameFragment, dentro del método onCreateView(), busca el código que configura los objetos de escucha de clics para los botones Go y Skip. Debajo de estas dos líneas, configura un objeto de escucha de clics para el botón End Game. Usa la variable de vinculación, binding. Dentro del objeto de escucha de clics, llama al método onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. En GameFragment, agrega un método llamado gameFinished() para navegar por la app a la pantalla de puntuación. Pasa la puntuación como un argumento mediante Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. En el método onEndGame(), llama al método gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. Ejecuta la app, juega y alterna unas palabras. Presiona el botón Finalizar juego. Ten en cuenta que la app navega a la pantalla de puntuación, pero no se muestra la puntuación final. Puedes solucionar este problema en la próxima tarea.

Cuando el usuario finaliza el juego, ScoreFragment no muestra el resultado. Quieres que un ViewModel retenga la puntuación para que se muestre en el ScoreFragment. Con el patrón de método de fábrica, pasarás el valor de puntuación durante la inicialización de ViewModel.

El patrón de método de fábrica es un patrón de diseño creativo que usa métodos de fábrica para crear objetos. Un método de fábrica es un método que muestra una instancia de la misma clase.

En esta tarea, crearás un ViewModel con un constructor parametrizado para el fragmento de puntuación y un método de fábrica a fin de crear una instancia de ViewModel.

  1. En el paquete score, crea una nueva clase de Kotlin llamada ScoreViewModel. Esta clase será el ViewModel del fragmento de puntuación.
  2. Extiende la clase ScoreViewModel desde ViewModel. Agrega un parámetro de constructor para la puntuación final. Agrega un bloque init con una instrucción de registro.
  3. En la clase ScoreViewModel, agrega una variable llamada score para guardar la puntuación final.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. En el paquete score, crea otra clase de Kotlin llamada ScoreViewModelFactory. Esta clase se encargará de crear las instancias del objeto ScoreViewModel.
  2. Extiende la clase ScoreViewModelFactory desde ViewModelProvider.Factory. Agrega un parámetro de constructor para la puntuación final.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. En ScoreViewModelFactory, Android Studio muestra un error sobre un miembro abstracto no implementado. Para resolver el error, anula el método create(). En el método create(), muestra el objeto ScoreViewModel recién construido.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. En ScoreFragment, crea variables de clase para ScoreViewModel y ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. En ScoreFragment, dentro de onCreateView(), después de inicializar la variable binding, inicializa el viewModelFactory. Usa el ScoreViewModelFactory Pasa la puntuación final del paquete de argumentos, como un parámetro de constructor para ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. En onCreateView(, después de inicializar viewModelFactory, inicializa el objeto viewModel. Llama al método ViewModelProviders.of(), pasa el contexto del fragmento de puntuación asociado y viewModelFactory. Esta acción creará el objeto ScoreViewModel con el método de fábrica definido en la clase viewModelFactory.
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. En el método onCreateView(), después de inicializar viewModel, establece el texto de la vista scoreText en la puntuación final definida en ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Ejecuta la app y juega. Desplázate por algunas o todas las palabras y presiona Finalizar el juego. Observa que el fragmento de puntuación ahora muestra la puntuación final.

  1. Opcional: Verifica los registros ScoreViewModel en Logcat mediante el filtrado en ScoreViewModel. Se debe mostrar el valor de la puntuación.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

En esta tarea, implementaste ScoreFragment para usar ViewModel. También aprendiste a crear un constructor parametrizado para una ViewModel mediante la interfaz ViewModelFactory.

¡Felicitaciones! Cambiaste la arquitectura de tu app para usar uno de los componentes de la arquitectura de Android, ViewModel. Se resolvió el problema del ciclo de vida de la app y, ahora, los datos del juego permanecen vigentes tras los cambios de configuración. También aprendiste a crear un constructor parametrizado para crear una ViewModel mediante la interfaz ViewModelFactory.

Proyecto de Android Studio: GuessTheWord

  • Los lineamientos de arquitectura de apps de Android recomiendan separar las clases que tienen responsabilidades diferentes.
  • Un controlador de IU es una clase basada en IU como Activity o Fragment. Los controladores de IU solo deberían contener lógica que se ocupe de interacciones del sistema operativo y de IU. No deberían contener datos para mostrar en la IU. Coloca esos datos en un ViewModel.
  • La clase ViewModel almacena y administra datos relacionados con la IU. La clase ViewModel permite que se conserven los datos luego de cambios de configuración, como las rotaciones de pantalla.
  • ViewModel es uno de los componentes de la arquitectura de Android recomendados.
  • ViewModelProvider.Factory es una interfaz que puedes usar para crear un objeto ViewModel.

En la siguiente tabla, se comparan los controladores de IU con las instancias de ViewModel que contienen datos:

Controlador de IU

ViewModel

Un ejemplo de un controlador de IU es el ScoreFragment que creaste en este codelab.

Un ejemplo de un objeto ViewModel es el ScoreViewModel que creaste en este codelab.

No contiene datos para mostrar en la IU.

Contiene datos que el controlador de IU muestra en la IU.

Contiene código para mostrar datos y código de eventos del usuario, como los objetos de escucha de clics.

Contiene el código para el procesamiento de datos.

Se destruye y se vuelve a crear durante cada cambio de configuración.

Se destruye solo cuando el controlador de IU asociado desaparece de forma permanente, para una actividad, cuando la actividad finaliza o para un fragmento, cuando se desconecta el fragmento.

Contiene vistas.

Nunca debería contener referencias a actividades, fragmentos o vistas, ya que no sobreviven a los cambios de configuración, pero la ViewModel sí lo hace.

Contiene una referencia al ViewModel asociado.

No contiene ninguna referencia al controlador de IU asociado.

Curso de Udacity:

Documentación para desarrolladores de Android:

Otro:

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

Para evitar perder datos durante un cambio en la configuración del dispositivo, ¿en qué clase debes guardar los datos de app?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Pregunta 2

Una ViewModel nunca debe contener referencias a fragmentos, actividades o vistas. ¿Verdadero o falso?

  • Verdadero
  • Falso

Pregunta 3

¿Cuándo se destruye un ViewModel?

  • Cuando se destruye el controlador de IU asociado y se vuelve a crear durante un cambio de orientación del dispositivo.
  • En un cambio de orientación.
  • Cuando finaliza el controlador de IU asociado (si es una actividad) o se desconecta (si es un fragmento).
  • Cuando el usuario presiona el botón Atrás.

Pregunta 4

¿Para qué sirve la interfaz ViewModelFactory?

  • Crea una instancia de un objeto ViewModel.
  • Retención de datos durante los cambios de orientación.
  • Actualizando los datos que se muestran en la pantalla.
  • Recibir notificaciones cuando se modifican los datos de la app.

Comienza la siguiente lección: 5.2: LiveData y observadores 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.