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 de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.

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 para almacenar y administrar datos relacionados con la IU de manera optimizada para los ciclos de vida. La clase ViewModel permite que los datos sobrevivan a cambios en la configuración del dispositivo, como las rotaciones de pantalla y los cambios en la disponibilidad del teclado.
  • Usas la clase ViewModelFactory para crear una instancia del objeto ViewModel y devolverlo, de modo que sobreviva 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 tu app
  • Cómo agregar 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 actividades y 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 ViewModel a la app para guardar sus datos de modo que sobrevivan a los cambios de configuración.
  • Usa ViewModelFactory y el patrón de diseño de método de fábrica para crear una instancia de un objeto ViewModel con parámetros de constructor.

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

Paso 1: Inicio

  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 siguiente palabra y disminuye la puntuación en uno, y el botón Entendido muestra la siguiente palabra y aumenta la puntuación en uno. El botón End Game no está implementado, por lo que no sucede nada cuando lo presionas.

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

  1. En Android Studio, explora el código para comprender cómo funciona la app.
  2. Asegúrate de consultar los archivos que se describen a continuación, ya que son particularmente importantes.

MainActivity.kt

Este archivo solo contiene código predeterminado generado por plantillas.

res/layout/main_activity.xml

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

Fragmentos de la IU

El código de partida 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

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

Este es el fragmento principal, en el que 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án en el juego.
  • El método onSkip() es el controlador de clics del botón Omitir. Disminuye la puntuación en 1 y, luego, muestra la siguiente palabra con el método nextWord().
  • El método onCorrect() es el controlador de clics del botón Entendido. 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 restarle.

screens/score/ScoreFragment.kt

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

res/navigation/main_navigation.xml

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

  • Desde el fragmento del 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 volver al fragmento del juego.

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

  1. Ejecuta el código de partida y juega con algunas palabras. Presiona Skip o Got It después de cada palabra.
  2. Ahora, la pantalla del juego muestra una palabra y la puntuación actual. Cambia la orientación de la pantalla girando el dispositivo o el emulador. Observa que se pierde la puntuación actual.
  3. Juega con algunas palabras más. Cuando se muestre la pantalla del juego con una puntuación, cierra y vuelve a abrir la app. Observa 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 partida 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 la app se cierra y se reinicia.
    Puedes resolver este problema con la devolución de llamada onSaveInstanceState(). Sin embargo, para usar el método onSaveInstanceState() debes escribir código adicional para 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 la app que aprenderás en este codelab.

Arquitectura de la app

La arquitectura de la app es una forma de diseñar las clases de tus apps 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 arquitectura de Android. La arquitectura de la app para Android es similar al patrón arquitectónico MVVM (model-view-viewmodel).

La app de GuessTheWord sigue el principio de diseño de separación de problemas y se divide en clases, en las que cada clase aborda un problema independiente. En este primer codelab de la lección, las clases con las que trabajarás son un controlador de IU, un ViewModel y un ViewModelFactory.

Controlador de IU

Un controlador de IU es una clase basada en la IU, como Activity o Fragment. Un controlador de IU solo debería contener lógica que se ocupe de interacciones del sistema operativo y de IU, como mostrar vistas y capturar la entrada del usuario. No coloques lógica de toma de decisiones, como la lógica 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. Según el principio de diseño de "separación de responsabilidades", el GameFragment solo es responsable de dibujar los 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 GameViewModel.

ViewModel

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

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

ViewModelFactory

Un ViewModelFactory crea instancias 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 los controladores de la 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 la app, el GameViewModel para el GameFragment. También aprenderás qué 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, la app de solución debería compilarse según lo esperado. Si no es así, intenta resolver el problema o revierte a la versión que se muestra a continuación.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. En la carpeta del paquete screens/game/, 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 tiene en cuenta el ciclo de vida, agrega un bloque init con una instrucción log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Paso 2: Anula onCleared() y agrega registros

El objeto 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 del juego

Se debe asociar un ViewModel con un controlador de IU. Para asociar los dos, 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, las instancias de ViewModel sobreviven. Si creas la instancia de 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 de ViewModel con un ViewModelProvider.

Cómo funciona ViewModelProvider:

  • ViewModelProvider devuelve un ViewModel existente si hay uno, o crea uno nuevo si aún no existe.
  • ViewModelProvider crea una instancia de ViewModel en asociación con el alcance determinado (una actividad o un fragmento).
  • El ViewModel creado se retiene mientras el alcance esté activo. Por ejemplo, si el alcance es un fragmento, el ViewModel se conserva hasta que se desconecta el fragmento.

Inicializa 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. Sobre 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 de Logcat y filtra en Game. Presiona el botón Reproducir en tu dispositivo o emulador. Se abre 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 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. Pero 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 navega fuera del fragmento del 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!

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

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

Para comparar, a continuación, se muestra cómo se controlan los datos de la 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 pasa por 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 necesita mostrar ahora son ViewModel. Cuando la app pasa por un cambio de configuración, el ViewModel sobrevive y los datos se conservan.

En esta tarea, moverás los datos de la IU de la app a la clase GameViewModel, junto con los métodos para procesar los datos. Esto se hace para que los datos se conserven durante los cambios de configuración.

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

Mueve los siguientes campos y métodos de datos 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, porque contiene referencias a las vistas. Esta variable se usa para expandir 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 la pantalla.
  3. Desde dentro del método onCreateView(), mueve las llamadas a los métodos 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 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 debe moverse al 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 de la clase GameViewModel después de la refactorización:

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 de la clase GameFragment después de la refactorización:

/**
* 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 que usen las variables GameViewModel, ya que ahora se encuentran 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 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. Mientras estás en la pantalla del juego, gira el dispositivo. Observa que la puntuación y la palabra actuales se conservan después del cambio de orientación.

¡Bien hecho! Ahora todos los datos de tu app se almacenan en un ViewModel, por lo que se conservan durante los cambios de configuración.

En esta tarea, implementarás el objeto de escucha de clics para el 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 Entendido y Omitir. Justo 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 hasta la pantalla de puntuación. Pasa la puntuación como un argumento con 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 prueba algunas palabras. Presiona el botón Finalizar juego . Observa que la app navega a la pantalla de puntuación, pero no se muestra la puntuación final. Corregirás esto en la siguiente tarea.

Cuando el usuario finaliza el juego, el ScoreFragment no muestra la puntuación. Quieres que un ViewModel contenga la puntuación que mostrará el ScoreFragment. Pasarás el valor de la puntuación durante la inicialización de ViewModel con el patrón de método de fábrica.

El patrón de método de fábrica es un patrón de diseño creacional que usa métodos de fábrica para crear objetos. Un método de fábrica es un método que devuelve 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 para crear una instancia del ViewModel.

  1. En el paquete score, crea una nueva clase de Kotlin llamada ScoreViewModel. Esta clase será el ViewModel para el fragmento de puntuación.
  2. Extiende la clase ScoreViewModel de 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 será responsable de crear una instancia 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(), devuelve 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 viewModelFactory. Usa ScoreViewModelFactory. Pasa la puntuación final del paquete de argumentos como un parámetro del constructor a 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. Esto 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. Recorre algunas palabras o todas y presiona Finalizar juego. Observa que el fragmento de la puntuación ahora muestra la puntuación final.

  1. Opcional: Verifica los registros de ScoreViewModel en Logcat filtrando por 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 un ViewModel con la interfaz ViewModelFactory.

¡Felicitaciones! Cambiaste la arquitectura de tu app para usar uno de los componentes de la arquitectura de Android, ViewModel. Resolviste el problema del ciclo de vida de la app y, ahora, los datos del juego sobreviven a los cambios de configuración. También aprendiste a crear un constructor con parámetros para crear un ViewModel, usando 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 la 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 los datos sobrevivan a 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 la IU con las instancias de ViewModel que contienen datos para ellos:

Controlador de IU

ViewModel

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

Un ejemplo de 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 objetos de escucha de clics.

Contiene 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 (en el caso de una actividad, cuando finaliza; en el caso de un fragmento, cuando se desconecta).

Contiene vistas.

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

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

Para evitar perder datos durante un cambio en la configuración del dispositivo, ¿en qué clase deberías guardar los datos de apps?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Pregunta 2

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

  • Verdadero
  • Falso

Pregunta 3

¿Cuándo se destruye 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?

  • Creando una instancia del objeto ViewModel.
  • Conservando los datos durante los cambios de orientación.
  • Actualizando los datos que se muestran en pantalla.
  • Recibiendo notificaciones cuando cambian los datos de app.

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