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
yButton
- 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.
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.
- Navega a la página provista del repositorio de GitHub del proyecto.
- 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.
- En la página de GitHub de este proyecto, haz clic en el botón Code, el cual abre una ventana emergente.
- 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.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- 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
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.
- En el navegador de archivos, ve hasta donde se encuentra la carpeta del proyecto descomprimida (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
Descripción general del código de inicio
- Abre el proyecto con el código de partida en Android Studio.
- Ejecuta la app en un dispositivo Android o en un emulador.
- 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.
- 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:
- Cuando haces clic en el botón Submit, la app no verifica la palabra del jugador. El jugador siempre gana puntos.
- No hay manera de terminar el juego. La app te permite jugar con más de 10 palabras.
- 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
ystyles
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
llamadasbinding
. - La función
onCreateView()
aumenta el XML de diseñogame_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 aonSubmitWord()
, 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()
yexitGame()
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:
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 |
Las actividades y los fragmentos son responsables de dibujar vistas y datos en la pantalla y responder a los eventos del usuario. |
|
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
.
- En la ventana Android de Android Studio, en la carpeta Gradle Scripts, abre el archivo
build.gradle(Module:Unscramble.app)
. - Para usar el
ViewModel
en tu app, verifica que tengas la dependencia de la biblioteca ViewModel dentro del bloquedependencies
. 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.
- 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.
- Asígnale el nombre
GameViewModel
y selecciona Class en la lista. - Cambia
GameViewModel
para que sea una subclase deViewModel
.ViewModel
es una clase abstracta, por lo que debes extenderla para usarla en tu app. Consulta la definición de claseGameViewModel
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
.
- En la parte superior de la clase
GameFragment
, agrega una propiedad de tipoGameViewModel
. - Inicializa el
GameViewModel
con el delegado de propiedadby viewModels()
de Kotlin. Obtendrás más información al respecto en la siguiente sección.
private val viewModel: GameViewModel by viewModels()
- 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
.
- Mueve las variables de datos
score
,currentWordCount
ycurrentScrambledWord
a la claseGameViewModel
.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- 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
esprivate
y mutable. Por lo tanto, solo es accesible y editable dentro de la claseViewModel
class. La convención es agregar un prefijo de guion bajo a la propiedadprivate
.
Fuera de la clase ViewModel
:
- El modificador de visibilidad predeterminado de Kotlin es
public
, por lo quecount
es público y accesible desde otras clases, como los controladores de IU. Debido a que solo se está anulando el métodoget()
, 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 delViewModel
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
- En
GameViewModel
, cambia la declaracióncurrentScrambledWord
para agregar una propiedad de copia de seguridad. Ahora_currentScrambledWord
solo es accesible y editable dentro deGameViewModel
. 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
- En
GameFragment
, actualiza el métodoupdateNextWordOnScreen()
para usar la propiedad de solo lecturaviewModel
,currentScrambledWord
.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
- En
GameFragment
, borra el código dentro de los métodosonSubmitWord()
yonSkipWord()
. 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:
Comprende el ciclo de vida de ViewModel
Agrega registros en GameViewModel
y GameFragment
para comprender mejor el ciclo de vida del ViewModel
.
- En
GameViewModel.kt
, agrega un bloqueinit
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.
- En la clase
GameViewModel
, anula el métodoonCleared()
. El objetoViewModel
se destruye cuando se desconecta el fragmento asociado o cuando finaliza la actividad. Justo antes de que se destruyaViewModel
, se llama a la devolución de llamadaonCleared()
. - Agrega una instrucción de registro dentro de
onCleared()
para hacer un seguimiento del ciclo de vida deGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
- En
GameFragment
dentro deonCreateView()
, 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 deonCreateView()
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
}
- En
GameFragment
, anula el método de devolución de llamadaonDetach()
, al que se llamará cuando se destruya la actividad y el fragmento correspondientes.
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- En Android Studio, ejecuta la app, abre la ventana Logcat y filtra en
GameFragment
. Observa que se crearonGameFragment
yGameViewModel
.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- 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 elGameViewModel
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!
- 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 llamadaonCleared()
. Se destruyeGameFragment
.
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 acurrentWord.
- Crea una palabra desordenada combinando las letras en
currentWord
y asígnala acurrentScrambledWord
. - 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
:
- En
GameViewModel,
, agrega una nueva variable de clase del tipoMutableList<String>
llamadawordsList
para contener una lista de palabras que usas en el juego a fin de evitar repeticiones. - Agrega otra variable de clase llamada
currentWord
para contener la palabra que el jugador intenta descifrar. Usa la palabra clavelateinit
, ya que inicializarás esta propiedad más tarde.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
- Agrega un nuevo método
private
llamadogetNextWord()
, arriba del bloqueinit
, sin parámetros y que no muestre nada. - Obtén una palabra aleatoria de
allWordsList
y asígnala acurrentWord
.
private fun getNextWord() {
currentWord = allWordsList.random()
}
- En
getNextWord()
, convierte la stringcurrentWord
en un array de caracteres y asígnala a una nuevaval
llamadatempWord
. 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.
- 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()
}
- Agrega un bloque
if-else
para comprobar si ya se usó una palabra. SiwordsList
contienecurrentWord
, llama agetNextWord()
. De lo contrario, actualiza el valor de_currentScrambledWord
con la palabra recién desordenada, aumenta la cantidad de palabras y agrega la palabra nueva awordsList
.
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
- 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.
- Ejecuta la app. Observa que la primera palabra siempre es "test".
- Para mostrar una palabra desordenada al comienzo de la app, debes llamar al método
getNextWord()
, que, a su vez, actualizacurrentScrambledWord
. Realiza una llamada al métodogetNextWord()
dentro del bloqueinit
deGameViewModel
.
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
- Agrega el modificador
lateinit
a la propiedad_currentScrambledWord
. Agrega una mención explícita del tipo de datosString
, ya que no se proporcionó un valor inicial.
private lateinit var _currentScrambledWord: String
- Ejecuta la app. Observa que se muestra una nueva palabra desordenada en el inicio de la app. ¡Genial!
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.
- En la clase
GameViewModel
, agrega otro método llamadonextWord().
Obtén la siguiente palabra de la lista y muestratrue
si el recuento de palabras es menor queMAX_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.
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
- Diálogo de alerta
- Título (opcional)
- Mensaje
- 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.
- Primero, agrega una propiedad de copia de seguridad a la variable
score
. EnGameViewModel
, cambia la declaración de variablescore
a lo siguiente.
private var _score = 0
val score: Int
get() = _score
- En
GameFragment
, agrega una función privada llamadashowFinalScoreDialog()
. Para crear un elementoMaterialAlertDialog
, usa la claseMaterialAlertDialogBuilder
a fin de crear partes del diálogo paso a paso. Llama al constructorMaterialAlertDialogBuilder
que pasa el contenido mediante el métodorequireContext()
del fragmento. El métodorequireContext()
muestra unContext
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
.
- 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))
- 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))
- Haz que no se pueda cancelar el diálogo de alerta cuando se presiona la tecla Back con el método
setCancelable()
y pasandofalse
.
.setCancelable(false)
- Agregue dos botones de texto EXIT y PLAY AGAIN con los métodos
setNegativeButton()
ysetPositiveButton()
. Llama aexitGame()
yrestartGame()
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
.
- Al final, agrega
show()
, que crea y muestra el diálogo de la alerta.
.show()
- 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
- Si aún no lo hiciste, en
GameFragment
, borra el código dentro deonSubmitWord()
, al cual se llama cuando se presiona el botón Submit. - Agrega una verificación al valor de muestra del método
viewModel.nextWord()
. Si haytrue
, otra palabra está disponible, por lo que debes actualizar la palabra desordenada en la pantalla medianteupdateNextWordOnScreen()
. 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()
}
}
- Ejecuta la app. Juega con algunas palabras. Recuerda que no implementaste el botón Skip, por lo que no puedes omitir palabras.
- 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.
Cómo agregar un método de ayuda para validar la palabra del jugador
- En
GameViewModel
, agrega un nuevo método privado llamadoincreaseScore()
sin parámetros y sin que se muestre un valor. Aumenta la variablescore
enSCORE_INCREASE
.
private fun increaseScore() {
_score += SCORE_INCREASE
}
- En
GameViewModel
, agrega un método de ayuda llamadoisUserWordCorrect()
que muestre unBoolean
y tome unaString
, la palabra del jugador, como parámetro. - 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.
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.
- En
GameFragment,
al principio deonSubmitWord()
, crea unval
llamadoplayerWord
. Para guardar la palabra del jugador, extráela del campo de texto en la variablebinding
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
- En
onSubmitWord()
, debajo de la declaración deplayerWord
, valida la palabra del jugador. Agrega una declaraciónif
para verificar la palabra del jugador con el métodoisUserWordCorrect()
, pasando elplayerWord
. - Dentro del bloque
if
, restablece el campo de texto y llama asetErrorTextField
y pasafalse
. - 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()
}
}
}
- Si la palabra del usuario es incorrecta, muestra un mensaje de error en el campo de texto. Agrega un bloque
else
al bloqueif
anterior y llama asetErrorTextField()
y pasatrue
. El métodoonSubmitWord()
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)
}
}
- 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.
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.
- De manera similar a
onSubmitWord()
, agrega una condición en el métodoonSkipWord()
. Si estrue
, muestra la palabra en la pantalla y restablece el campo de texto. Si esfalse
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()
}
}
- 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.
- En
GameViewModel
, haz clic con el botón derecho en la variablecurrentWordCount
, selecciona Refactor > Rename... . Agrega un guion bajo como prefijo al nombre nuevo:_currentWordCount
. - Agrega un campo de copia de seguridad.
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
- En
GameFragment
, dentro deonCreateView()
, 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}")
- 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
- 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. - Para restablecer los datos de app, agrega un método llamado
reinitializeData()
enGameViewModel
. Establece la puntuación y el recuento de palabras en0
. Borra la lista de palabras y llama al métodogetNextWord()
.
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
- En
GameFragment
, en la parte superior del métodorestartGame()
, realiza una llamada al método recién creado,reinitializeData()
.
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- 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.
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.
¡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
oFragment
. 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 unViewModel
. - La clase
ViewModel
almacena y administra datos relacionados con la IU. La claseViewModel
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
- Descripción general de ViewModel
- Guía de arquitectura de apps
- Práctica con componentes de Material para Android: diálogos
- Anatomía del diálogo de alerta
- MaterialAlertDialogBuilder
- Propiedades de copia de seguridad
- Componentes de la arquitectura de Android
- Diálogos de Material de Android
- Propiedades y campos: métodos get, métodos set, const, lateinit