Cómo almacenar datos en ViewModel

1. Antes de comenzar

En los codelabs anteriores, aprendiste el ciclo de vida de las actividades y los fragmentos, y los problemas relacionados del ciclo de vida con los cambios de configuración. Para guardar los datos de la app, guardar el estado de la instancia es una opción, pero viene con sus propias limitaciones. En este codelab, aprenderás una forma sólida de diseñar tu app y preservar los datos de estas durante los cambios de configuración aprovechando las bibliotecas de Android Jetpack.

Las bibliotecas de Android Jetpack son una colección de bibliotecas que te facilitarán el desarrollo de apps de Android geniales. Estas bibliotecas te ayudan a seguir prácticas recomendadas, te liberan de escribir código estándar y simplifican tareas complejas para que puedas concentrarte en el código que te interesa, como la lógica de la app.

Los componentes de la arquitectura de Android forman parte de las bibliotecas de Android Jetpack para ayudarte a diseñar apps con una buena arquitectura. Los componentes de la arquitectura proporcionan orientación sobre la arquitectura de las apps, y es la práctica recomendada.

La arquitectura de apps es un conjunto de reglas de diseño. Al igual que el plano de una casa, tu arquitectura proporciona la estructura para tu aplicación. Una buena arquitectura de la app puede hacer que tu código sea robusto, flexible, escalable y utilizable durante años.

En este codelab, aprenderás a usar ViewModel, uno de los componentes de la arquitectura para almacenar los datos de tu app. Los datos almacenados no se pierden si el framework destruye y vuelve a crear las actividades y los fragmentos durante un cambio de configuración o en otros eventos.

Requisitos previos

  • Cómo descargar el código fuente de GitHub y abrirlo en Android Studio
  • Cómo crear y ejecutar una app básica de Android en Kotlin a través de actividades y fragmentos
  • Conocimiento sobre el campo de texto de Material y los widgets de IU comunes, como TextView y Button
  • Cómo usar la vinculación de vista en la app
  • Aspectos básicos del ciclo de vida de la actividad y del fragmento
  • Cómo agregar información de registro a una app y leer registros con Logcat en Android Studio.

Qué aprenderás

  • Introducción a los conceptos básicos de la arquitectura de apps para Android
  • Cómo usar la clase ViewModel en tu app
  • Cómo retener datos de la IU mediante cambios en la configuración de dispositivos mediante un ViewModel
  • Propiedades de copia de seguridad en Kotlin
  • Cómo usar MaterialAlertDialog de la biblioteca de componentes de Material Design

Qué compilarás

  • Una app de juego llamda Unscramble donde el usuario puede adivinar las palabras desordenadas.

Requisitos

  • Una computadora que tenga Android Studio instalado
  • Código de inicio de la app de Unscramble

2. Descripción general de la app de inicio

Descripción general del videojuego

La app de Unscramble es un juego de palabras desordenadas de un solo jugador. La app muestra una palabra con las letras desordenadas a la vez, y el jugador debe adivinar la palabra usando todas las letras de la palabra. El jugador gana puntos si la palabra es correcta; de lo contrario, puede volver a intentarlo. La app también tiene la opción de omitir la palabra actual. En la esquina superior izquierda, la app muestra el recuento de palabras, que es la cantidad de palabras que se usaron en este juego actual. Hay 10 palabras por partido.

8edd6191a40a57e1.png 992bf57f066caf49.png b82a9817b5ec4d11.png

Descarga el código de partida

Este codelab te brinda el código de partida para que lo extiendas con funciones que se explican en este codelab. Es posible que el código de inicio incluya código que te resulte conocido y desconocido por los codelabs anteriores. Aprenderás más sobre el código desconocido en codelabs posteriores.

Si usas el código de partida de GitHub, ten en cuenta que el nombre de la carpeta es android-basics-kotlin-unscramble-app-starter. Selecciona esta carpeta cuando abras el proyecto en Android Studio.

  1. Navega a la página provista del repositorio de GitHub del proyecto.
  2. Verifica que el nombre de la rama coincida con el especificado en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

1e4c0d2c081a8fd2.png

  1. En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta del proyecto descomprimida (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.

Descripción general del código de inicio

  1. Abre el proyecto con el código de partida en Android Studio.
  2. Ejecuta la app en un dispositivo Android o en un emulador.
  3. Para jugar el juego, presiona las opciones Submit y Skip. Observa que, cuando presionas los botones, se muestra la siguiente palabra y se incrementa el recuento de palabras.
  4. Observa que la puntuación solo aumenta si se presiona el botón Submit.

Problemas con el código de partida

Mientras jugabas, es posible que hayas visto los siguientes errores:

  1. Cuando haces clic en el botón Submit, la app no verifica la palabra del jugador. El jugador siempre gana puntos.
  2. No hay manera de terminar el juego. La app te permite jugar con más de 10 palabras.
  3. La pantalla del juego muestra una palabra desordenada, la puntuación del jugador y el recuento de palabras. Cambia la orientación de la pantalla girando el dispositivo o el emulador. Observa que la palabra, la puntuación y el recuento de palabras actuales se pierden y el juego se reinicia desde el principio.

Problemas principales en la app

La app de partida no guarda ni restablece el estado de la app ni los datos durante los cambios de configuración, como cuando cambia la orientación del dispositivo.

Puedes solucionar 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 lógica para recuperar ese estado. Además, la cantidad de datos que se pueden almacenar es mínima.

Puedes resolver estos problemas usando los componentes de la arquitectura de Android que aprenderás en esta ruta de aprendizaje.

Explicación del código de inicio

El código de partida que descargaste ya tiene creado el diseño de pantalla del juego. En esta ruta de aprendizaje, te enfocarás en implementar la lógica del juego. Usarás componentes de la arquitectura para implementar la arquitectura recomendada de la app y resolver los problemas mencionados anteriormente. Aquí encontrarás una breve explicación de algunos de los archivos para comenzar.

game_fragment.xml

  • Abre res/layout/game_fragment.xml en la vista Design.
  • Contiene el diseño de la única pantalla de tu app que es la pantalla del juego.
  • Este diseño contiene un campo de texto para la palabra del jugador, junto con TextViews para mostrar la puntuación y el recuento de palabras. También incluye instrucciones y botones (Submit y Skip) para jugar.

main_activity.xml

Define el diseño de la actividad principal con un solo fragmento de juego.

carpeta res/values

Estás familiarizado con los archivos de recursos de esta carpeta.

  • colors.xml contiene los colores del tema que se usaron en la app.
  • strings.xml contiene todas las strings que necesita tu app.
  • Las carpetas themes y styles contienen la personalización de la IU completada para tu app.

MainActivity.kt

Contiene el código predeterminado que genera la plantilla para establecer la vista de contenido de la actividad como main_activity.xml.

ListOfWords.kt

Este archivo contiene una lista de palabras usadas en el juego, así como las constantes para la cantidad máxima de palabras por juego y la cantidad de puntos que el jugador ganará por cada palabra correcta.

GameFragment.kt

Es el único fragmento de tu app, en el que se produce la mayor parte de la acción del juego:

  • Las variables se definen para la palabra desordenada (currentScrambledWord), el recuento de palabras (currentWordCount) y la puntuación (score) actuales.
  • Se define la instancia de objeto vinculante con acceso a las vistas game_fragment llamadas binding.
  • La función onCreateView() aumenta el XML de diseño game_fragment con el objeto de vinculación.
  • La función onViewCreated() configura los objetos de escucha de clics en el botón y actualiza la IU.
  • onSubmitWord() es el objeto de escucha de clics para el botón Submit. Esta función muestra la siguiente palabra desordenada, borra el campo de texto y aumenta la puntuación y el recuento de palabras sin validar la palabra del jugador.
  • onSkipWord() es el objeto de escucha de clics del botón Skip. Esta función actualiza la IU similar a onSubmitWord(), excepto la puntuación.
  • getNextScrambledWord() es una función auxiliar que elige una palabra aleatoria de la lista y mezcla sus letras.
  • Las funciones restartGame() y exitGame() se usan para reiniciar y finalizar el juego respectivamente. Las usarás más adelante.
  • setErrorTextField() borra el contenido del campo de texto y restablece el estado de error.
  • La función updateNextWordOnScreen() muestra la nueva palabra desordenada.

3. Obtén información sobre la arquitectura de la app

La arquitectura te proporciona los lineamientos para ayudarte a asignar responsabilidades en tu app, entre las clases. Una arquitectura de app bien diseñada te ayuda a escalar tu app y a extenderla con funciones adicionales en el futuro. También facilita la colaboración.

Los principios arquitectónicos más comunes son la separación de problemas y el control de la IU a partir de un modelo.

Separación de problemas

El principio de separación de problemas indica que la app debe dividirse en clases, cada una con responsabilidades independientes.

Cómo controlar la IU a partir de un modelo

Otro principio importante es que debes controlar la IU a partir de un modelo, preferentemente uno de persistencia. Los modelos son componentes responsables de administrar los datos de una app. Son independientes de los componentes de la app y los objetos Views, de modo que no se ven afectados por el ciclo de vida de la app y los problemas asociados.

Las clases o los componentes principales de la arquitectura de Android son el controlador de IU (actividad/fragmento), ViewModel, LiveData y Room. Estos componentes se encargan de la complejidad del ciclo de vida y ayudan a evitar problemas relacionados con el ciclo de vida. Aprenderás sobre LiveData y Room en codelabs posteriores.

Puedes ver una porción básica de la arquitectura en este diagrama:

597074ed0d08947b.png

Controlador de IU (Actividad/Fragmento)

Las actividades y los fragmentos son controladores de IU. Los controladores de IU controlan las IU dibujando vistas en la pantalla, capturando eventos de los usuarios y todo lo relacionado con la IU con la que el usuario interactúa. Los datos de la app o cualquier lógica de toma de decisiones relacionados con esos datos no deberían estar en las clases de los controladores de IU.

El sistema Android puede destruir los controladores de IU en cualquier momento en función de ciertas interacciones del usuario o debido a condiciones del sistema, como memoria insuficiente. Debido a que no puedes controlar estos eventos, no debes almacenar datos ni estados de la app en los controladores de IU. En su lugar, la lógica de toma de decisiones sobre los datos debe agregarse en tu ViewModel.

Por ejemplo, en tu app de Unscramble, la palabra desordenada, la puntuación y el recuento de palabras se muestran en un fragmento (controlador de IU). El código de toma de decisiones, como determinar la siguiente palabra desordenada y los cálculos de la puntuación y el recuento de palabras, deben estar en tu ViewModel.

ViewModel

ViewModel es un modelo de los datos de app que se muestran en las vistas. Los modelos son componentes responsables de manejar los datos de una app. Permiten que tu app siga el principio de arquitectura de controlar la IU a partir del modelo.

El elemento ViewModel almacena los datos relacionados con la app que no se destruyen cuando el framework de Android destruye la actividad o el fragmento y los recrea. Los objetos ViewModel se retienen automáticamente (no se destruyen como la actividad o una instancia de fragmento) durante los cambios de configuración, de manera que los datos que conservan están disponibles de inmediato para la siguiente instancia de fragmento o actividad.

Para implementar ViewModel en tu app, extiende la clase ViewModel, que es de la biblioteca de componentes de la arquitectura, y almacena los datos de app en esa clase.

En resumen:

Responsabilidades de fragmento/actividad (controlador de IU)

Responsabilidades de ViewModel

Las actividades y los fragmentos son responsables de dibujar vistas y datos en la pantalla y responder a los eventos del usuario.

ViewModel es responsable de retener y procesar todos los datos necesarios para la IU. Nunca debes acceder a tu jerarquía de vistas (como un objeto de vinculación de vista) ni retener una referencia a la actividad o al fragmento.

4. Agrega un ViewModel

En esta tarea, agregarás un ViewModel a tu app para almacenar los datos de app (palabra desordenada, recuento de palabras y puntuación).

Tu app tendrá la siguiente arquitectura. MainActivity contiene un GameFragment, y GameFragment tendrá acceso a información sobre el juego desde GameViewModel.

2b29a13dde3481c3.png

  1. En la ventana Android de Android Studio, en la carpeta Gradle Scripts, abre el archivo build.gradle(Module:Unscramble.app).
  2. Para usar el ViewModel en tu app, verifica que tengas la dependencia de la biblioteca ViewModel dentro del bloque dependencies. Este paso ya está listo. Según la versión más reciente de la biblioteca, es posible que sea diferente el número de versión de esta en el código generado.
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

Se recomienda usar siempre la versión más reciente de la biblioteca a pesar de que la versión que se mencione en el codelab sea otra.

  1. Crea un archivo de clase de Kotlin nuevo llamado GameViewModel. En la ventana de Android, haz clic con el botón derecho en la carpeta ui.game. Selecciona New > Kotlin File/Class.

d48361a4f73d4acb.png

  1. Asígnale el nombre GameViewModel y selecciona Class en la lista.
  2. Cambia GameViewModel para que sea una subclase de ViewModel. ViewModel es una clase abstracta, por lo que debes extenderla para usarla en tu app. Consulta la definición de clase GameViewModel a continuación.
class GameViewModel : ViewModel() {
}

Cómo adjuntar el ViewModel al fragmento

Para asociar un ViewModel a un controlador de IU (actividad/fragmento), crea una referencia (objeto) en ViewModel dentro del controlador de IU.

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

  1. En la parte superior de la clase GameFragment, agrega una propiedad de tipo GameViewModel.
  2. Inicializa el GameViewModel con el delegado de propiedad by viewModels() de Kotlin. Obtendrás más información al respecto en la siguiente sección.
private val viewModel: GameViewModel by viewModels()
  1. Si Android Studio lo solicita, importa androidx.fragment.app.viewModels.

Delegado de propiedad de Kotlin

En Kotlin, cada propiedad mutable (var) tiene funciones del método get y el método set que se generan automáticamente para ella. Se llama a las funciones del método get y el método set cuando asignas un valor o lees el valor de la propiedad.

En el caso de una propiedad de solo lectura (val), difiere levemente de una propiedad mutable. Solo la función de método get se genera de forma predeterminada. Se llama a esta función de método get cuando lees el valor de una propiedad de solo lectura.

La delegación de propiedades en Kotlin te permite transferir la responsabilidad del método get y el método set a una clase diferente.

Esta clase (que se denomina clase de delegado) brinda funciones del método get y el método set de la propiedad y controla sus cambios.

Una propiedad de delegado se define mediante la cláusula by y una instancia de clase delegada:

// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()

En tu app, si inicializas el modelo de vista con el constructor GameViewModel predeterminado, como en el siguiente ejemplo:

private val viewModel = GameViewModel()

Luego, la app perderá el estado de la referencia viewModel cuando el dispositivo pase por un cambio de configuración. Por ejemplo, si giras el dispositivo, la actividad se destruye y se vuelve a crear, y tendrás una nueva instancia del modelo de vista con el estado inicial nuevamente.

En su lugar, usa el enfoque de delegado de propiedad y delega la responsabilidad del objeto viewModel a una clase separada llamada viewModels. Esto significa que cuando accedes al objeto viewModel, se maneja de forma interna por la clase delegada, viewModels. La clase delegada crea el objeto viewModel por ti en el primer acceso y retiene su valor mediante los cambios de configuración y muestra el valor cuando se solicita.

5. Mueve datos al ViewModel

Separar los datos de IU de tu app desde el controlador de IU (tus clases Activity/Fragment) te permite seguir mejor el principio de responsabilidad individual que mencionamos más arriba. Tus actividades y fragmentos son responsables de dibujar vistas y datos en la pantalla, mientras que tu ViewModel es responsable de retener y procesar todos los datos necesarios para la IU.

En esta tarea, moverás las variables de datos de GameFragment a la clase GameViewModel.

  1. Mueve las variables de datos score, currentWordCount y currentScrambledWord a la clase GameViewModel.
class GameViewModel : ViewModel() {

    private var score = 0
    private var currentWordCount = 0
    private var currentScrambledWord = "test"
...
  1. Observa los errores relacionados con las referencias sin resolver. Esto se debe a que las propiedades son privadas para ViewModel y su controlador de IU no puede acceder a ellas. A continuación, corregirás estos errores.

Para resolver este problema, no puedes hacer los modificadores de visibilidad de las propiedades public; otras clases no pueden editar los datos. Esto es riesgoso, ya que una clase externa podría cambiar los datos de formas inesperadas que no siguen las reglas del juego especificadas en el modelo de vista. Por ejemplo, una clase externa podría cambiar score a un valor negativo.

Dentro de ViewModel, los datos deben poder editarse, por lo que deberían ser private y var. A partir de ViewModel, los datos deben ser legibles, pero no editables. Por lo tanto, los datos deben exponerse como public y val. Para lograr este comportamiento, Kotlin tiene una función llamada propiedad de copia de seguridad.

Propiedad de copia de seguridad

Una propiedad de copia de seguridad te permite mostrar algo de un método get que no sea el objeto exacto.

Ya aprendiste que, para cada propiedad, el framework de Kotlin genera métodos get y métodos set.

Para los métodos get y métodos set, puedes anular uno o ambos métodos, y proporcionar tu propio comportamiento personalizado. Para implementar una propiedad de copia de seguridad, anularás el método get para mostrar una versión de solo lectura de tus datos. Ejemplo de propiedad de copia de seguridad:

// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0

// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
   get() = _count

Por ejemplo, en tu app deseas que los datos de app sean privados para el ViewModel:

Dentro de la clase ViewModel:

  • La propiedad _count es private y mutable. Por lo tanto, solo es accesible y editable dentro de la clase ViewModel class. La convención es agregar un prefijo de guion bajo a la propiedad private.

Fuera de la clase ViewModel:

  • El modificador de visibilidad predeterminado de Kotlin es public, por lo que count es público y accesible desde otras clases, como los controladores de IU. Debido a que solo se está anulando el método get(), esta propiedad es inmutable y de solo lectura. Cuando una clase externa accede a esta propiedad, muestra el valor de _count y su valor no se puede modificar. Esto protege los datos de app dentro del ViewModel contra cambios no deseados y no seguros por parte de clases externas, pero permite que emisores externos accedan de manera segura a su valor.

Cómo agregar la propiedad de copia de seguridad a currentScrambledWord

  1. En GameViewModel, cambia la declaración currentScrambledWord para agregar una propiedad de copia de seguridad. Ahora _currentScrambledWord solo es accesible y editable dentro de GameViewModel. El controlador de IU, GameFragment, puede leer su valor con la propiedad de solo lectura, currentScrambledWord.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
   get() = _currentScrambledWord
  1. En GameFragment, actualiza el método updateNextWordOnScreen() para usar la propiedad de solo lectura viewModel, currentScrambledWord.
private fun updateNextWordOnScreen() {
   binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
  1. En GameFragment, borra el código dentro de los métodos onSubmitWord() y onSkipWord(). Implementarás estos métodos más tarde. Deberías poder compilar el código ahora sin errores.

6. El ciclo de vida de un ViewModel

El framework mantiene activo el ViewModel siempre y cuando esté dentro del alcance de la actividad o del fragmento. Un elemento ViewModel no se destruye si el propietario se destruye por un cambio de configuración, como la rotación de pantalla. La nueva instancia del propietario se vuelve a conectar a la instancia ViewModel existente, como se muestra en el siguiente diagrama:

91227008b74bf4bb.png

Comprende el ciclo de vida de ViewModel

Agrega registros en GameViewModel y GameFragment para comprender mejor el ciclo de vida del ViewModel.

  1. En GameViewModel.kt, agrega un bloque init con una instrucción de registro.
class GameViewModel : ViewModel() {
   init {
       Log.d("GameFragment", "GameViewModel created!")
   }

   ...
}

Kotlin proporciona el bloque de inicializador (también conocido como bloque init) como lugar para el código de configuración inicial necesario durante la inicialización de una instancia de objeto. Los bloques de inicializador tienen el prefijoinit palabra clave seguida de llaves{} para crear el adjunto de VLAN de supervisión. Este bloque de código se ejecuta cuando se crea e inicializa la instancia de objeto por primera vez.

  1. En la clase GameViewModel, anula el método onCleared(). 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().
  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.d("GameFragment", "GameViewModel destroyed!")
}
  1. En GameFragment dentro de onCreateView(), después de obtener una referencia al objeto de vinculación, agrega una instrucción de registro para registrar la creación del fragmento. La devolución de llamada de onCreateView() se activará cuando se cree el fragmento por primera vez y también cada vez que se vuelva a crear para eventos como los cambios de configuración.
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View {
   binding = GameFragmentBinding.inflate(inflater, container, false)
   Log.d("GameFragment", "GameFragment created/re-created!")
   return binding.root
}
  1. En GameFragment, anula el método de devolución de llamada onDetach(), al que se llamará cuando se destruya la actividad y el fragmento correspondientes.
override fun onDetach() {
    super.onDetach()
    Log.d("GameFragment", "GameFragment destroyed!")
}
  1. En Android Studio, ejecuta la app, abre la ventana Logcat y filtra en GameFragment. Observa que se crearon GameFragment y GameViewModel.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
  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, pero el GameViewModel se crea solo una vez y no se vuelve a crear ni se destruye por cada llamada.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
  1. Sal del juego o sal de la app con la flecha hacia atrás. GameViewModel se destruye y se llama a la devolución de llamada onCleared(). Se destruye GameFragment.
com.example.android.unscramble D/GameFragment: GameViewModel destroyed!
com.example.android.unscramble D/GameFragment: GameFragment destroyed!

7. Completa ViewModel

En esta tarea, completarás con más detalle el GameViewModel con métodos auxiliares para obtener la siguiente palabra, validar la palabra del jugador para aumentar la puntuación y revisar el recuento de palabras para finalizar el juego.

Inicialización tardía

Por lo general, cuando se declara una variable, se le proporciona un valor inicial por anticipado. Sin embargo, si aún no estás listo para asignar un valor, puedes inicializarlo más adelante. Para inicializar tarde una propiedad en Kotlin, usa la palabra clave lateinit, que significa "inicialización tardía". Si te aseguras de que inicializarás la propiedad antes de usarla, puedes declararla con lateinit. La memoria no se asigna a la variable hasta que se inicializa. Si intentas acceder a la variable antes de inicializarla, la app fallará.

Obtén la palabra siguiente

Crea el método getNextWord() en la clase GameViewModel, con la siguiente funcionalidad:

  • Obtén una palabra aleatoria de allWordsList y asígnala a currentWord.
  • Crea una palabra desordenada combinando las letras en currentWord y asígnala a currentScrambledWord.
  • Maneja el caso en el que la palabra desordenada es la misma que la palabra ordenada.
  • Asegúrate de no mostrar la misma palabra dos veces durante el juego.

Implementa los siguientes pasos en la clase GameViewModel:

  1. En GameViewModel,, agrega una nueva variable de clase del tipo MutableList<String> llamada wordsList para contener una lista de palabras que usas en el juego a fin de evitar repeticiones.
  2. Agrega otra variable de clase llamada currentWord para contener la palabra que el jugador intenta descifrar. Usa la palabra clave lateinit, ya que inicializarás esta propiedad más tarde.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
  1. Agrega un nuevo método private llamado getNextWord(), arriba del bloque init, sin parámetros y que no muestre nada.
  2. Obtén una palabra aleatoria de allWordsList y asígnala a currentWord.
private fun getNextWord() {
   currentWord = allWordsList.random()
}
  1. En getNextWord(), convierte la string currentWord en un array de caracteres y asígnala a una nueva val llamada tempWord. Para desordenar la palabra, mezcla los caracteres de este array con el método de Kotlin, shuffle().
val tempWord = currentWord.toCharArray()
tempWord.shuffle()

Un Array es similar a MutableList, pero tiene un tamaño fijo cuando se inicializa. Un elemento Array no puede expandir ni contraer su tamaño (debes copiar un array para cambiar su tamaño), mientras que una MutableList tiene funciones add() y remove() para que pueda aumentar y disminuir el tamaño.

  1. En ocasiones, el orden aleatorio de los caracteres es el mismo que el de la palabra original. Agrega el siguiente bucle while alrededor de la llamada para cambiar el orden de modo aleatorio a fin de continuar el bucle hasta que la palabra desordenada no sea la misma que la palabra original.
while (String(tempWord).equals(currentWord, false)) {
    tempWord.shuffle()
}
  1. Agrega un bloque if-else para comprobar si ya se usó una palabra. Si wordsList contiene currentWord, llama a getNextWord(). De lo contrario, actualiza el valor de _currentScrambledWord con la palabra recién desordenada, aumenta la cantidad de palabras y agrega la palabra nueva a wordsList.
if (wordsList.contains(currentWord)) {
    getNextWord()
} else {
    _currentScrambledWord = String(tempWord)
    ++currentWordCount
    wordsList.add(currentWord)
}
  1. Este es el método getNextWord() completo para tu referencia.
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
   currentWord = allWordsList.random()
   val tempWord = currentWord.toCharArray()
   tempWord.shuffle()

   while (String(tempWord).equals(currentWord, false)) {
       tempWord.shuffle()
   }
   if (wordsList.contains(currentWord)) {
       getNextWord()
   } else {
       _currentScrambledWord = String(tempWord)
       ++currentWordCount
       wordsList.add(currentWord)
   }
}

Inicialización tardía de currentScrambledWord

Ahora creaste el método getNextWord() para obtener la siguiente palabra desordenada. Lo llamarás cuando se inicialice el objeto GameViewModel por primera vez. Usa el bloque init para inicializar las propiedades lateinit en la clase, como la palabra actual. El resultado será que la primera palabra que se muestra en la pantalla será una palabra desordenada en lugar de test.

  1. Ejecuta la app. Observa que la primera palabra siempre es "test".
  2. Para mostrar una palabra desordenada al comienzo de la app, debes llamar al método getNextWord(), que, a su vez, actualiza currentScrambledWord. Realiza una llamada al método getNextWord() dentro del bloque init de GameViewModel.
init {
    Log.d("GameFragment", "GameViewModel created!")
    getNextWord()
}
  1. Agrega el modificador lateinit a la propiedad _currentScrambledWord. Agrega una mención explícita del tipo de datos String, ya que no se proporcionó un valor inicial.
private lateinit var _currentScrambledWord: String
  1. Ejecuta la app. Observa que se muestra una nueva palabra desordenada en el inicio de la app. ¡Genial!

8edd6191a40a57e1.png

Cómo agregar un método de ayuda

A continuación, agrega un método de ayuda para procesar y modificar los datos dentro del ViewModel. Usarás este método en tareas posteriores.

  1. En la clase GameViewModel, agrega otro método llamado nextWord(). Obtén la siguiente palabra de la lista y muestra true si el recuento de palabras es menor que MAX_NO_OF_WORDS.
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
    return if (currentWordCount < MAX_NO_OF_WORDS) {
        getNextWord()
        true
    } else false
}

8. Diálogos

En el código de partida, el juego nunca terminó, incluso después de que se jugaran 10 palabras. Modifica tu app para que, cuando el usuario haya jugado las 10 palabras, el juego termine y aparezca un diálogo con la puntuación final. También le darás al usuario una opción para volver a jugar o salir del juego.

62aa368820ffbe31.png

Esta es la primera vez que agregas un diálogo a una app. Un diálogo es una ventana pequeña (pantalla) que le pide al usuario que tome una decisión o ingrese información adicional. Normalmente, un diálogo no ocupa toda la pantalla, y requiere que los usuarios realicen una acción para poder continuar. Android ofrece diferentes tipos de diálogos. En este codelab, aprenderás sobre los diálogos de alerta.

Anatomía del diálogo de alerta

f8650ca15e854fe4.png

  1. Diálogo de alerta
  2. Título (opcional)
  3. Mensaje
  4. Botones de texto

Cómo implementar el diálogo de puntuación final

Usa MaterialAlertDialog de la biblioteca de componentes de Material Design para agregar un diálogo a la app que cumpla con los lineamientos de Material. Debido a que un diálogo está relacionado con la IU, el GameFragment es responsable de crear y mostrar el diálogo de puntuación final.

  1. Primero, agrega una propiedad de copia de seguridad a la variable score. En GameViewModel, cambia la declaración de variable score a lo siguiente.
private var _score = 0
val score: Int
   get() = _score
  1. En GameFragment, agrega una función privada llamada showFinalScoreDialog(). Para crear un elemento MaterialAlertDialog, usa la clase MaterialAlertDialogBuilder a fin de crear partes del diálogo paso a paso. Llama al constructor MaterialAlertDialogBuilder que pasa el contenido mediante el método requireContext() del fragmento. El método requireContext() muestra un Context no nulo.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
}

Como sugiere el nombre, Context hace referencia al contexto o el estado actual de una aplicación, una actividad o un fragmento. Contiene información sobre la actividad, el fragmento o la aplicación. Por lo general, se usa para obtener acceso a los recursos, las bases de datos y otros servicios del sistema. En este paso, pasas el contexto del fragmento para crear el diálogo de alerta.

Si Android Studio te lo solicita, import com.google.android.material.dialog.MaterialAlertDialogBuilder.

  1. Agrega el código para configurar el título del diálogo de alerta. Usa un recurso de strings de strings.xml.
MaterialAlertDialogBuilder(requireContext())
   .setTitle(getString(R.string.congratulations))
  1. Configura el mensaje para que muestre la puntuación final. Usa la versión de solo lectura de la variable de puntuación (viewModel.score), que agregaste antes.
   .setMessage(getString(R.string.you_scored, viewModel.score))
  1. Haz que no se pueda cancelar el diálogo de alerta cuando se presiona la tecla Back con el método setCancelable() y pasando false.
    .setCancelable(false)
  1. Agregue dos botones de texto EXIT y PLAY AGAIN con los métodos setNegativeButton() y setPositiveButton(). Llama a exitGame() y restartGame() respectivamente desde las lambdas.
    .setNegativeButton(getString(R.string.exit)) { _, _ ->
        exitGame()
    }
    .setPositiveButton(getString(R.string.play_again)) { _, _ ->
        restartGame()
    }

Esta sintaxis puede ser nueva para ti, pero es una abreviatura de setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()}), en la que el método setNegativeButton() toma dos parámetros: un String y una función, DialogInterface.OnClickListener() que se pueden expresar como lambda. Cuando el último argumento que se pasa es una función, puedes colocar la expresión lambda fuera de los paréntesis. Esto se conoce como sintaxis de expresión lambda final. Se aceptan las dos maneras de escribir el código (con la lambda dentro o fuera de los paréntesis). Lo mismo se aplica a la función setPositiveButton.

  1. Al final, agrega show(), que crea y muestra el diálogo de la alerta.
      .show()
  1. Aquí se encuentra el método showFinalScoreDialog() completo para referencia.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
       .setTitle(getString(R.string.congratulations))
       .setMessage(getString(R.string.you_scored, viewModel.score))
       .setCancelable(false)
       .setNegativeButton(getString(R.string.exit)) { _, _ ->
           exitGame()
       }
       .setPositiveButton(getString(R.string.play_again)) { _, _ ->
           restartGame()
       }
       .show()
}

9. Implementa OnClickListener para el botón de envío

En esta tarea, usarás ViewModel y el diálogo de alerta que agregaste a fin de implementar la lógica del juego para el objeto de escucha de clics del botón Submit.

Muestra las palabras desordenadas

  1. Si aún no lo hiciste, en GameFragment, borra el código dentro de onSubmitWord(), al cual se llama cuando se presiona el botón Submit.
  2. Agrega una verificación al valor de muestra del método viewModel.nextWord(). Si hay true, otra palabra está disponible, por lo que debes actualizar la palabra desordenada en la pantalla mediante updateNextWordOnScreen(). De lo contrario, el juego termina, así que muestra el diálogo de alerta con la puntuación final.
private fun onSubmitWord() {
    if (viewModel.nextWord()) {
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Ejecuta la app. Juega con algunas palabras. Recuerda que no implementaste el botón Skip, por lo que no puedes omitir palabras.
  2. Observa que el campo de texto no está actualizado, por lo que el jugador debe borrar manualmente la palabra anterior. La puntuación final en el diálogo de alerta es siempre cero. Solucionarás estos errores en los próximos pasos.

a4c660e212ce2c31.png 12a42987a0edd2c4.png

Cómo agregar un método de ayuda para validar la palabra del jugador

  1. En GameViewModel, agrega un nuevo método privado llamado increaseScore() sin parámetros y sin que se muestre un valor. Aumenta la variable score en SCORE_INCREASE.
private fun increaseScore() {
   _score += SCORE_INCREASE
}
  1. En GameViewModel, agrega un método de ayuda llamado isUserWordCorrect() que muestre un Boolean y tome una String, la palabra del jugador, como parámetro.
  2. En isUserWordCorrect(), valida la palabra del jugador y aumenta la puntuación si lo que adivinó es correcto. Esto actualizará la puntuación final en tu diálogo de alerta.
fun isUserWordCorrect(playerWord: String): Boolean {
   if (playerWord.equals(currentWord, true)) {
       increaseScore()
       return true
   }
   return false
}

Actualiza el campo de texto.

Cómo mostrar errores en el campo de texto

Para los campos de texto de Material, TextInputLayout viene con la funcionalidad integrada para mostrar mensajes de error. Por ejemplo, en el siguiente campo de texto, se cambia el color de la etiqueta, se muestra un ícono de error, un mensaje de error, etcétera.

520cc685ae1317ac.png

Para mostrar un error en el campo de texto, puedes configurar el mensaje de error de forma dinámica en código o de manera estática en el archivo de diseño. A continuación, se muestra un ejemplo de cómo establecer y restablecer el error en el código:

// Set error text
passwordLayout.error = getString(R.string.error)

// Clear error text
passwordLayout.error = null

En el código de partida, encontrarás que el método de ayuda setErrorTextField(error: Boolean) ya está definido para ayudarte a establecer y restablecer el error en el campo de texto. Llama a este método con true o false como parámetro de entrada según si deseas que aparezca un error en el campo de texto o no.

Fragmento de código en el código de partida

private fun setErrorTextField(error: Boolean) {
   if (error) {
       binding.textField.isErrorEnabled = true
       binding.textField.error = getString(R.string.try_again)
   } else {
       binding.textField.isErrorEnabled = false
       binding.textInputEditText.text = null
   }
}

En esta tarea, implementarás el método onSubmitWord(). Cuando se envía una palabra, valida lo que adivinó el usuario comparándolo con la palabra original. Si la palabra es correcta, ve a la siguiente palabra (o muestra el diálogo si el juego terminó). Si la palabra es incorrecta, muestra un error en el campo de texto y permanece en la palabra actual.

  1. En GameFragment, al principio de onSubmitWord(), crea un val llamado playerWord. Para guardar la palabra del jugador, extráela del campo de texto en la variable binding.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()
    ...
}
  1. En onSubmitWord(), debajo de la declaración de playerWord, valida la palabra del jugador. Agrega una declaración if para verificar la palabra del jugador con el método isUserWordCorrect(), pasando el playerWord.
  2. Dentro del bloque if, restablece el campo de texto y llama a setErrorTextField y pasa false.
  3. Mueve el código existente dentro del bloque if.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }
}
  1. Si la palabra del usuario es incorrecta, muestra un mensaje de error en el campo de texto. Agrega un bloque else al bloque if anterior y llama a setErrorTextField() y pasa true. El método onSubmitWord() completo debería verse de la siguiente manera:
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (viewModel.nextWord()) {
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    } else {
        setErrorTextField(true)
    }
}
  1. Ejecuta la app. Juega con otras palabras. Si la palabra del jugador es correcta, la palabra queda vacía al hacer clic en el botón Submit; de lo contrario, aparece un mensaje que dice "Try again!". Tenga en cuenta que el botón Skip aún no funciona. Agregarás esta implementación en la siguiente tarea.

a10c7d77aa26b9db.png

10. Implementa el botón Omitir

En esta tarea, agregarás la implementación para onSkipWord(), que controla lo que ocurre cuando se hace clic en el botón Skip.

  1. De manera similar a onSubmitWord(), agrega una condición en el método onSkipWord(). Si es true, muestra la palabra en la pantalla y restablece el campo de texto. Si es false y no hay más palabras en esta ronda, muestra el diálogo de alerta con la puntuación final.
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
    if (viewModel.nextWord()) {
        setErrorTextField(false)
        updateNextWordOnScreen()
    } else {
        showFinalScoreDialog()
    }
}
  1. Ejecuta la app. Juega. Observa que los botones Skip y Submit funcionan según lo previsto. ¡Exacto!

11. Cómo verificar los datos de ViewModel

Para esta tarea, agrega registros en GameFragment a fin de observar que tus datos de app se conserven en ViewModel durante los cambios de configuración. Para acceder a currentWordCount en GameFragment, debes exponer una versión de solo lectura con una propiedad de copia de seguridad.

  1. En GameViewModel, haz clic con el botón derecho en la variable currentWordCount, selecciona Refactor > Rename... . Agrega un guion bajo como prefijo al nombre nuevo: _currentWordCount.
  2. Agrega un campo de copia de seguridad.
private var _currentWordCount = 0
val currentWordCount: Int
   get() = _currentWordCount
  1. En GameFragment, dentro de onCreateView(), sobre la instrucción de devolución, agrega otro registro para imprimir los datos de la app, la palabra, la puntuación y el recuento de palabras.
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
       "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
  1. En Android Studio, abre Logcat y filtra por GameFragment. Ejecuta tu app y juega con algunas palabras. Cambia la orientación de tu dispositivo. El fragmento (controlador de IU) se destruye y se vuelve a crear. Observa los registros. Ahora puedes ver un aumento en la puntuación y el recuento de palabras.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: GameViewModel created!
com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
com.example.android.unscramble D/GameFragment: GameFragment destroyed!
com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9

Observa que los datos de app se conservan en ViewModel durante los cambios de orientación. Actualizarás el valor de la puntuación y el recuento de palabras en la IU con LiveData y la vinculación de datos en codelabs posteriores.

12. Actualiza la lógica de reinicio del juego

  1. Vuelve a ejecutar la app y juega con todas las palabras. En el diálogo de alerta de Congratulations!, haz clic en PLAY AGAIN. La app no te permitirá volver a jugar porque el recuento de palabras alcanzó el valor MAX_NO_OF_WORDS. Debes restablecer el recuento de palabras a 0 para volver a jugar desde el principio.
  2. Para restablecer los datos de app, agrega un método llamado reinitializeData() en GameViewModel. Establece la puntuación y el recuento de palabras en 0. Borra la lista de palabras y llama al método getNextWord().
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
   _score = 0
   _currentWordCount = 0
   wordsList.clear()
   getNextWord()
}
  1. En GameFragment, en la parte superior del método restartGame(), realiza una llamada al método recién creado, reinitializeData().
private fun restartGame() {
   viewModel.reinitializeData()
   setErrorTextField(false)
   updateNextWordOnScreen()
}
  1. Vuelve a ejecutar tu app. Juega. Cuando veas el diálogo de felicitaciones, haz clic en Play Again. Ahora deberías poder volver a jugar correctamente.

Así es como debería verse tu app final. El juego muestra diez palabras con las letras desordenadas al azar para que el jugador las ordene. Puedes omitir la palabra con Skip o adivinar una palabra y presionar Submit. Si la adivinas correctamente, la puntuación aumenta. Una respuesta incorrecta muestra un estado de error en el campo de texto. Con cada palabra nueva, el recuento de palabras también aumenta.

Ten en cuenta que la puntuación y el recuento de palabras que se muestran en la pantalla aún no se actualizan. Sin embargo, la información aún se almacena en el modelo de vista y se conserva durante los cambios de configuración, como la rotación del dispositivo. Actualizarás la puntuación y el recuento de palabras en la pantalla en codelabs posteriores.

f332979d6f63d0e5.png 2803d4855f5d401f.png

Después de 10 palabras, el juego termina y aparece un diálogo de alerta con la puntuación final y una opción para salir de la partida o jugar de nuevo.

d8e0111f5f160ead.png

¡Felicitaciones! Creaste tu primer ViewModel y guardaste los datos.

13. Código de solución

GameFragment.kt

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/**
 * Fragment where the game is played, contains the game logic.
 */
class GameFragment : Fragment() {

    private val viewModel: GameViewModel by viewModels()

    // Binding object instance with access to the views in the game_fragment.xml layout
    private lateinit var binding: GameFragmentBinding

    // Create a ViewModel the first time the fragment is created.
    // If the fragment is re-created, it receives the same GameViewModel instance created by the
    // first fragment

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Inflate the layout XML file and return a binding object instance
        binding = GameFragmentBinding.inflate(inflater, container, false)
        Log.d("GameFragment", "GameFragment created/re-created!")
        Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
                "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Setup a click listener for the Submit and Skip buttons.
        binding.submit.setOnClickListener { onSubmitWord() }
        binding.skip.setOnClickListener { onSkipWord() }
        // Update the UI
        updateNextWordOnScreen()
        binding.score.text = getString(R.string.score, 0)
        binding.wordCount.text = getString(
            R.string.word_count, 0, MAX_NO_OF_WORDS)
    }

    /*
    * Checks the user's word, and updates the score accordingly.
    * Displays the next scrambled word.
    * After the last word, the user is shown a Dialog with the final score.
    */
    private fun onSubmitWord() {
        val playerWord = binding.textInputEditText.text.toString()

        if (viewModel.isUserWordCorrect(playerWord)) {
            setErrorTextField(false)
            if (viewModel.nextWord()) {
                updateNextWordOnScreen()
            } else {
                showFinalScoreDialog()
            }
        } else {
            setErrorTextField(true)
        }
    }

    /*
    * Skips the current word without changing the score.
    */
    private fun onSkipWord() {
        if (viewModel.nextWord()) {
            setErrorTextField(false)
            updateNextWordOnScreen()
        } else {
            showFinalScoreDialog()
        }
    }

    /*
     * Gets a random word for the list of words and shuffles the letters in it.
     */
    private fun getNextScrambledWord(): String {
        val tempWord = allWordsList.random().toCharArray()
        tempWord.shuffle()
        return String(tempWord)
    }

    /*
    * Creates and shows an AlertDialog with the final score.
    */
    private fun showFinalScoreDialog() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(getString(R.string.congratulations))
            .setMessage(getString(R.string.you_scored, viewModel.score))
            .setCancelable(false)
            .setNegativeButton(getString(R.string.exit)) { _, _ ->
                exitGame()
            }
            .setPositiveButton(getString(R.string.play_again)) { _, _ ->
                restartGame()
            }
            .show()
    }

    /*
     * Re-initializes the data in the ViewModel and updates the views with the new data, to
     * restart the game.
     */
    private fun restartGame() {
        viewModel.reinitializeData()
        setErrorTextField(false)
        updateNextWordOnScreen()
    }

    /*
     * Exits the game.
     */
    private fun exitGame() {
        activity?.finish()
    }

    override fun onDetach() {
        super.onDetach()
        Log.d("GameFragment", "GameFragment destroyed!")
    }

    /*
    * Sets and resets the text field error status.
    */
    private fun setErrorTextField(error: Boolean) {
        if (error) {
            binding.textField.isErrorEnabled = true
            binding.textField.error = getString(R.string.try_again)
        } else {
            binding.textField.isErrorEnabled = false
            binding.textInputEditText.text = null
        }
    }

    /*
     * Displays the next scrambled word on screen.
     */
    private fun updateNextWordOnScreen() {
        binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
    }
}

GameViewModel.kt

import android.util.Log
import androidx.lifecycle.ViewModel

/**
 * ViewModel containing the app data and methods to process the data
 */
class GameViewModel : ViewModel(){
    private var _score = 0
    val score: Int
        get() = _score

    private var _currentWordCount = 0
    val currentWordCount: Int
        get() = _currentWordCount

    private lateinit var _currentScrambledWord: String
    val currentScrambledWord: String
        get() = _currentScrambledWord

    // List of words used in the game
    private var wordsList: MutableList<String> = mutableListOf()
    private lateinit var currentWord: String

    init {
        Log.d("GameFragment", "GameViewModel created!")
        getNextWord()
    }

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

    /*
    * Updates currentWord and currentScrambledWord with the next word.
    */
    private fun getNextWord() {
        currentWord = allWordsList.random()
        val tempWord = currentWord.toCharArray()
        tempWord.shuffle()

        while (String(tempWord).equals(currentWord, false)) {
            tempWord.shuffle()
        }
        if (wordsList.contains(currentWord)) {
            getNextWord()
        } else {
            _currentScrambledWord = String(tempWord)
            ++_currentWordCount
            wordsList.add(currentWord)
        }
    }

    /*
    * Re-initializes the game data to restart the game.
    */
    fun reinitializeData() {
       _score = 0
       _currentWordCount = 0
       wordsList.clear()
       getNextWord()
    }

    /*
    * Increases the game score if the player's word is correct.
    */
    private fun increaseScore() {
        _score += SCORE_INCREASE
    }

    /*
    * Returns true if the player word is correct.
    * Increases the score accordingly.
    */
    fun isUserWordCorrect(playerWord: String): Boolean {
        if (playerWord.equals(currentWord, true)) {
            increaseScore()
            return true
        }
        return false
    }

    /*
    * Returns true if the current word count is less than MAX_NO_OF_WORDS
    */
    fun nextWord(): Boolean {
        return if (_currentWordCount < MAX_NO_OF_WORDS) {
            getNextWord()
            true
        } else false
    }
}

14. Resumen

  • Los lineamientos de arquitectura de apps para Android recomiendan separar las clases que tienen responsabilidades diferentes y controlar la IU a partir de un modelo.
  • 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 ser la fuente de datos para mostrar en la IU. Coloca esos datos y cualquier lógica relacionada 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 recomendados de la arquitectura de Android.

15. Más información