Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia. Todos los codelabs del curso se detallan en la página de destino de codelabs sobre los aspectos básicos de Kotlin para Android.
Pantalla de título | Pantalla del juego | Pantalla de puntuación |
Introducción
En este codelab, aprenderás sobre uno de los componentes de la arquitectura de Android, ViewModel
:
- Usas la clase
ViewModel
a fin de almacenar y administrar datos relacionados con la IU de una manera optimizada para los ciclos de vida. La claseViewModel
permite que se conserven los datos luego de los cambios de configuración del dispositivo, como las rotaciones de pantalla y la disponibilidad del teclado. - Usa la clase
ViewModelFactory
para crear una instancia y mostrar el objetoViewModel
que sobrevive a los cambios de configuración.
Conocimientos que ya deberías tener
- Cómo crear apps básicas para Android en Kotlin
- Cómo usar el gráfico de navegación para implementar la navegación en la app
- Cómo agregar un código para navegar entre los destinos de tu app y pasar datos entre los destinos de navegación
- Cómo funcionan los ciclos de vida de las actividades y los fragmentos
- Cómo agregar información de registro a una app y leer registros con Logcat en Android Studio
Qué aprenderás
- Cómo usar la arquitectura de apps de Android recomendada
- Cómo usar las clases
Lifecycle
,ViewModel
yViewModelFactory
en tu app - Cómo retener datos de la IU mediante cambios en la configuración de dispositivos
- Qué es el patrón de diseño del método de fábrica y cómo usarlo
- Cómo crear un objeto
ViewModel
con la interfazViewModelProvider.Factory
Actividades
- Agrega un objeto
ViewModel
a la app para guardar los datos de app y sobrevivir a los cambios de configuración. - Usa
ViewModelFactory
y el patrón de diseño del método de fábrica para crear una instancia de un objetoViewModel
con parámetros del constructor.
En los codelabs de la clase 5, desarrollarás la app de GuessTheWord, que comienza con un código de inicio. GuessTheWord es un juego de dos carpas con dos jugadores en el que los jugadores colaboran para obtener la puntuación más alta posible.
El primer jugador mira las palabras en la app y actúa a su vez de manera individual, asegurándose de no mostrarle la palabra al segundo jugador. El segundo jugador intenta adivinar la palabra.
Para jugar, el primer jugador abre la app en el dispositivo y ve una palabra, como "guitarra", como se muestra en la siguiente captura de pantalla.
El primer jugador representa la palabra y tiene cuidado de no decirla realmente.
- Cuando el segundo jugador adivina la palabra correctamente, el primero presiona el botón Entendido, lo que aumenta el recuento en uno y muestra la palabra siguiente.
- Si el segundo jugador no puede adivinar la palabra, el primer jugador presiona el botón Omitir, lo que disminuye la cantidad de palabras y pasa a la siguiente palabra.
- Para finalizar el juego, presiona el botón Finalizar juego. (Esta función no se encuentra en el código de inicio para el primer codelab de la serie).
En esta tarea, descargarás y ejecutarás la app de inicio y examinarás el código.
Paso 1: Comienza
- Descarga el código de inicio de GuessTheWord y abre el proyecto en Android Studio.
- Ejecuta la app en un dispositivo con Android o en un emulador.
- Presiona los botones. Observa que el botón Omitir muestra la palabra siguiente y disminuye la puntuación en uno, y el botón Entendido muestra la siguiente palabra y aumenta la puntuación en una. El botón Finalizar juego no está implementado, por lo que no sucede nada cuando lo presionas.
Paso 2: Realiza una explicación del código
- En Android Studio, explora el código para familiarizarte con el funcionamiento de la app.
- Asegúrate de revisar los archivos que se describen a continuación, que son particularmente importantes.
MainActivity.kt
Este archivo solo contiene el código predeterminado generado por la plantilla.
res/diseño/principal_actividad.xml
Este archivo contiene el diseño principal de la app. El objeto NavHostFragment
aloja los otros fragmentos a medida que el usuario navega por la app.
Fragmentos de la IU
El código de inicio tiene tres fragmentos en tres paquetes diferentes en el paquete com.example.android.guesstheword.screens
:
title/TitleFragment
para la pantalla de títulogame/GameFragment
para la pantalla del juegoscore/ScoreFragment
para la pantalla de puntuación
pantallas/title/TitleFragment.kt
El fragmento de título es la primera pantalla que se muestra cuando se inicia la app. Para navegar a la pantalla del juego, se configura un controlador de clics en el botón Play.
pantallas/juego/GameFragment.kt
Este es el fragmento principal, donde se produce la mayor parte de la acción del juego:
- Las variables se definen para la palabra actual y la puntuación actual.
- El
wordList
definido dentro del métodoresetList()
es una lista de muestra de palabras que se usará en el juego. - El método
onSkip()
es el controlador de clics del botón Skip. Se reduce la puntuación a 1 y, luego, se muestra la palabra siguiente con el métodonextWord()
. - El método
onCorrect()
es el controlador de clics del botón Go. Este método se implementa de manera similar al métodoonSkip()
. La única diferencia es que este método suma 1 a la puntuación en lugar de restarla.
pantallas/score/ScoreFragment.kt
ScoreFragment
es la pantalla final del juego y muestra el resultado final del jugador. En este codelab, agregarás la implementación para mostrar esta pantalla y mostrar la puntuación final.
res/navigation/main_navigation.xml
En el gráfico de navegación, se muestra cómo se conectan los fragmentos a través de la navegación:
- Desde el fragmento de título, el usuario puede navegar al fragmento del juego.
- Desde el fragmento del juego, el usuario puede navegar al fragmento de la puntuación.
- Desde el fragmento de puntuación, el usuario puede regresar al fragmento del juego.
En esta tarea, encontrarás problemas con la app de inicio de GuessTheWord.
- Ejecuta el código de inicio y juega con varias palabras; para ello, presiona Omitir o Entendido después de cada palabra.
- La pantalla del juego ahora muestra una palabra y la puntuación actual. Cambia la orientación de la pantalla girando el dispositivo o el emulador. Ten en cuenta que se perdió la puntuación actual.
- Ejecuta el juego con algunas palabras más. Cuando se muestre la pantalla del juego con alguna puntuación, cierra y vuelve a abrir la app. Ten en cuenta que el juego se reinicia desde el principio, ya que no se guarda el estado de la app.
- Juega con algunas palabras y, luego, presiona el botón End Game. Observa que no sucede nada.
Problemas en la app:
- La app de inicio no guarda ni restablece el estado de la app durante los cambios de configuración, como cuando cambia la orientación del dispositivo o cuando se cierra y se reinicia la app.
Puedes resolver este problema usando la devolución de llamadaonSaveInstanceState()
. Sin embargo, para usar el métodoonSaveInstanceState()
, debes escribir código adicional a fin de guardar el estado en un paquete y, luego, implementar la lógica para recuperar ese estado. Además, la cantidad de datos que se pueden almacenar es mínima. - La pantalla del juego no navega a la pantalla de puntuación cuando el usuario presiona el botón End Game.
Puedes resolver estos problemas usando los componentes de la arquitectura de apps que aprenderás en este codelab.
Arquitectura de la app
La arquitectura de apps es una forma de diseñar tus apps y clases, y las relaciones entre ellas, de modo que el código esté organizado, funcione bien en situaciones particulares y sea fácil de usar. En este conjunto de cuatro codelabs, las mejoras que realices en la app de GuessTheWord seguirán los lineamientos de la arquitectura de apps para Android, y usarás los componentes de la arquitectura de Android. La arquitectura de la app para Android es similar al patrón de arquitectura MVVM (model-view-viewmodel).
La app de GuessTheWord sigue el principio de diseño de separación de problemas y se divide en clases, y cada clase aborda una inquietud diferente. En este primer codelab de la clase, las clases con las que trabajas son un controlador de IU, una ViewModel
y una ViewModelFactory
.
Controlador de IU
Un controlador de IU es una clase basada en IU, como Activity
o Fragment
. Un controlador de IU solo debe contener lógica que se ocupe de interacciones del sistema operativo y de IU, como mostrar vistas y capturar las entradas del usuario. No uses la lógica de toma de decisiones, como la que determina el texto que se mostrará, en el controlador de IU.
En el código de inicio de GuessTheWord, los controladores de IU son los tres fragmentos: GameFragment
, ScoreFragment,
y TitleFragment
. De acuerdo con el principio de separación de problemas, GameFragment
solo es responsable de dibujar elementos del juego en la pantalla y saber cuándo el usuario presiona los botones, y nada más. Cuando el usuario presiona un botón, esta información se pasa a la GameViewModel
.
ViewModel
Una ViewModel
contiene datos que se mostrarán en un fragmento o una actividad asociada con la ViewModel
. Un ViewModel
puede hacer cálculos y transformaciones simples en los datos a fin de prepararlos para que el controlador de la IU los muestre. En esta arquitectura, ViewModel
toma las decisiones.
El GameViewModel
contiene datos como el valor de puntuación, la lista de palabras y la palabra actual, ya que estos son los datos que se mostrarán en la pantalla. GameViewModel
también contiene la lógica empresarial para realizar cálculos simples a fin de decidir cuál es el estado actual de los datos.
ViewModelFactory
Un objeto ViewModelFactory
crea una instancia de objetos ViewModel
, con o sin parámetros de constructor.
En codelabs posteriores, aprenderás sobre otros componentes de la arquitectura de Android relacionados con controladores de IU y ViewModel
.
La clase ViewModel
está diseñada para almacenar y administrar los datos relacionados con la IU. En esta app, cada ViewModel
se asocia con un fragmento.
En esta tarea, agregarás tu primer ViewModel
a tu app, GameViewModel
para GameFragment
. También aprenderás lo que significa que ViewModel
está optimizado para los ciclos de vida.
Paso 1: Agrega la clase GameViewModel
- Abre el archivo
build.gradle(module:app)
. Dentro del bloquedependencies
, agrega la dependencia de Gradle paraViewModel
.
Si usas la versión más reciente de la biblioteca, se compilará la app de la solución como se espera. Si no es así, intenta resolver el problema o vuelve a la versión que se muestra a continuación.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- En la carpeta
screens/game/
del paquete, crea una nueva clase de Kotlin llamadaGameViewModel
. - Haz que la clase
GameViewModel
extienda la clase abstractaViewModel
. - Para ayudarte a comprender mejor cómo
ViewModel
está optimizado para los ciclos de vida, agrega un bloqueinit
con una sentencialog
.
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
Paso 2: Anula onCleared() y agrega registros
El elemento ViewModel
se destruye cuando se desconecta el fragmento asociado o cuando finaliza la actividad. Justo antes de que se destruya ViewModel
, se llama a la devolución de llamada onCleared()
para limpiar los recursos.
- En la clase
GameViewModel
, anula el métodoonCleared()
. - Agrega una instrucción de registro dentro de
onCleared()
para hacer un seguimiento del ciclo de vida deGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
Paso 3: Asocia GameViewModel con el fragmento de juego
Se debe asociar un ViewModel
con un controlador de IU. Para asociar ambos, crea una referencia al ViewModel
dentro del controlador de IU.
En este paso, crearás una referencia de GameViewModel
dentro del controlador de IU correspondiente, que es GameFragment
.
- En la clase
GameFragment
, agrega un campo del tipoGameViewModel
en el nivel superior como una variable de clase.
private lateinit var viewModel: GameViewModel
Paso 4: Inicializa el ViewModel
Durante los cambios de configuración, como las rotaciones de pantalla, se vuelven a crear los controladores de IU, como los fragmentos. Sin embargo, ViewModel
instancias sobreviven. Si creas la instancia ViewModel
con la clase ViewModel
, se creará un objeto nuevo cada vez que se vuelva a crear el fragmento. En su lugar, crea la instancia ViewModel
con un elemento ViewModelProvider
.
Cómo funciona ViewModelProvider
:
ViewModelProvider
muestra unaViewModel
existente si existe, o crea una nueva si aún no existe.ViewModelProvider
crea una instancia deViewModel
asociada con el alcance determinado (una actividad o un fragmento).- Se conservará la
ViewModel
creada siempre que el alcance esté activo. Por ejemplo, si el alcance es un fragmento, se retiene elViewModel
hasta que se desconecta el fragmento.
Inicializa el ViewModel
con el método ViewModelProviders.of()
para crear un ViewModelProvider
:
- En la clase
GameFragment
, inicializa la variableviewModel
. Coloca este código dentro deonCreateView()
, después de la definición de la variable de vinculación. Usa el métodoViewModelProviders.of()
y pasa el contextoGameFragment
asociado y la claseGameViewModel
. - Arriba de la inicialización del objeto
ViewModel
, agrega una instrucción de registro para registrar la llamada al métodoViewModelProviders.of()
.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
- Ejecuta la app. En Android Studio, abre el panel Logcat y filtra en
Game
. Presiona el botón Reproducir en tu dispositivo o emulador. Se abrirá la pantalla del juego.
Como se muestra en Logcat, el métodoonCreateView()
deGameFragment
llama al métodoViewModelProviders.of()
para crear elGameViewModel
. Las instrucciones de registro que agregaste aGameFragment
yGameViewModel
aparecen en el Logcat.
- Habilita la configuración de girar automáticamente en tu dispositivo o emulador y cambia la orientación de la pantalla varias veces.
GameFragment
se destruye y se vuelve a crear cada vez, por lo que se llama aViewModelProviders.of()
cada vez. Sin embargo, elGameViewModel
se crea solo una vez y no se vuelve a crear ni se destruye por cada llamada.
I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of
- Sal del juego o sal del fragmento de juego. Se destruye
GameFragment
. También se destruye elGameViewModel
asociado y se llama a la devolución de llamadaonCleared()
.
I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel created! I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameFragment: Called ViewModelProviders.of I/GameViewModel: GameViewModel destroyed!
ViewModel
sobrevive a los cambios de configuración, por lo que es un buen lugar para los datos que necesitan sobrevivir a los cambios de configuración:
- Coloca los datos que se mostrarán en la pantalla y el código para procesar esos datos, en el
ViewModel
. - El elemento
ViewModel
nunca debe contener referencias a fragmentos, actividades o vistas, ya que estas no sobreviven a los cambios de configuración.
A modo de comparación, aquí se muestra cómo se controlan los datos de IU de GameFragment
en la app de inicio antes de agregar ViewModel
y después de agregar ViewModel
:
- Antes de agregar
ViewModel
:
Cuando la app atraviesa un cambio de configuración, como una rotación de pantalla, el fragmento del juego se destruye y se vuelve a crear. Se pierden los datos. - Después de agregar
ViewModel
y mover los datos de la IU del fragmento del juego aViewModel
:
Todos los datos que el fragmento debe mostrar ahora sonViewModel
. Cuando la app pasa por un cambio de configuración, elViewModel
permanece vigente y se conservan los datos.
En esta tarea, moverás los datos de la IU de la app a la clase GameViewModel
y los métodos para procesar los datos. Esto sirve para conservar los datos durante los cambios de configuración.
Paso 1: Mueve los campos de datos y el procesamiento de datos al ViewModel
Mueve los siguientes campos de datos y métodos de GameFragment
a GameViewModel
:
- Mueve los campos de datos
word
,score
ywordList
. Asegúrate de queword
yscore
no seanprivate
.
No muevas la variable de vinculaciónGameFragmentBinding
, ya que contiene referencias a las vistas. Esta variable se usa para aumentar el diseño, configurar los objetos de escucha de clics y mostrar los datos en la pantalla (responsabilidades del fragmento). - Mueve los métodos
resetList()
ynextWord()
. Estos métodos deciden qué palabra mostrar en pantalla. - Desde el método
onCreateView()
, mueve las llamadas de método aresetList()
ynextWord()
al bloqueinit
deGameViewModel
.
Estos métodos deben estar en el bloqueinit
, ya que debes restablecer la lista de palabras cuando se crea elViewModel
, no cada vez que se crea el fragmento. Puedes borrar la instrucción de registro en el bloqueinit
deGameFragment
.
Los controladores de clics onSkip()
y onCorrect()
en GameFragment
contienen código para procesar los datos y actualizar la IU. El código para actualizar la IU debe permanecer en el fragmento, pero el código para procesar los datos se debe mover a ViewModel
.
Por ahora, coloca los métodos idénticos en ambos lugares:
- Copia los métodos
onSkip()
yonCorrect()
deGameFragment
aGameViewModel
. - En
GameViewModel
, asegúrate de que los métodosonSkip()
yonCorrect()
no seanprivate
, ya que harás referencia a estos métodos desde el fragmento.
Este es el código para la clase GameViewModel
, después de refactorizar:
class GameViewModel : ViewModel() {
// The current word
var word = ""
// The current score
var score = 0
// The list of words - the front of the list is the next word to guess
private lateinit var wordList: MutableList<String>
/**
* Resets the list of words and randomizes the order
*/
private fun resetList() {
wordList = mutableListOf(
"queen",
"hospital",
"basketball",
"cat",
"change",
"snail",
"soup",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll",
"bubble"
)
wordList.shuffle()
}
init {
resetList()
nextWord()
Log.i("GameViewModel", "GameViewModel created!")
}
/**
* Moves to the next word in the list
*/
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word = wordList.removeAt(0)
}
updateWordText()
updateScoreText()
}
/** Methods for buttons presses **/
fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
}
Este es el código para la clase GameFragment
, después de refactorizar:
/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {
private lateinit var binding: GameFragmentBinding
private lateinit var viewModel: GameViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.game_fragment,
container,
false
)
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
updateScoreText()
updateWordText()
return binding.root
}
/** Methods for button click handlers **/
private fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
private fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = word
}
private fun updateScoreText() {
binding.scoreText.text = score.toString()
}
}
Paso 2: Actualiza las referencias a los controladores de clics y los campos de datos en GameFragment
- En
GameFragment
, actualiza los métodosonSkip()
yonCorrect()
. Quita el código para actualizar la puntuación y, en su lugar, llama a los métodosonSkip()
yonCorrect()
correspondientes enviewModel
. - Como moviste el método
nextWord()
aViewModel
, el fragmento del juego ya no puede acceder a él.
EnGameFragment
, en los métodosonSkip()
yonCorrect()
, reemplaza la llamada anextWord()
porupdateScoreText()
yupdateWordText()
. Estos métodos muestran los datos en la pantalla.
private fun onSkip() {
viewModel.onSkip()
updateWordText()
updateScoreText()
}
private fun onCorrect() {
viewModel.onCorrect()
updateScoreText()
updateWordText()
}
- En
GameFragment
, actualiza las variablesscore
yword
para usar las variablesGameViewModel
, que ahora están enGameViewModel
.
private fun updateWordText() {
binding.wordText.text = viewModel.word
}
private fun updateScoreText() {
binding.scoreText.text = viewModel.score.toString()
}
- En
GameViewModel
, dentro del métodonextWord()
, quita las llamadas a los métodosupdateWordText()
yupdateScoreText()
. Ahora se llama a estos métodos desde elGameFragment
. - Compila la app y asegúrate de que no haya errores. Si tienes errores, limpia y vuelve a compilar el proyecto.
- Ejecuta la app y juega con algunas palabras. En la pantalla del juego, rota el dispositivo. Observa que la puntuación actual y la palabra actual se conservan después del cambio de orientación.
Buen trabajo. Ahora todos tus datos se almacenan en una ViewModel
, por lo que se conservan durante los cambios de configuración.
En esta tarea, implementarás el objeto de escucha de clics del botón End Game.
- En
GameFragment
, agrega un método llamadoonEndGame()
. Se llamará al métodoonEndGame()
cuando el usuario presione el botón End Game.
private fun onEndGame() {
}
- En
GameFragment
, dentro del métodoonCreateView()
, busca el código que configura los objetos de escucha de clics para los botones Go y Skip. Debajo de estas dos líneas, configura un objeto de escucha de clics para el botón End Game. Usa la variable de vinculación,binding
. Dentro del objeto de escucha de clics, llama al métodoonEndGame()
.
binding.endGameButton.setOnClickListener { onEndGame() }
- En
GameFragment
, agrega un método llamadogameFinished()
para navegar por la app a la pantalla de puntuación. Pasa la puntuación como un argumento mediante Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
val action = GameFragmentDirections.actionGameToScore()
action.score = viewModel.score
NavHostFragment.findNavController(this).navigate(action)
}
- En el método
onEndGame()
, llama al métodogameFinished()
.
private fun onEndGame() {
gameFinished()
}
- Ejecuta la app, juega y alterna unas palabras. Presiona el botón Finalizar juego. Ten en cuenta que la app navega a la pantalla de puntuación, pero no se muestra la puntuación final. Puedes solucionar este problema en la próxima tarea.
Cuando el usuario finaliza el juego, ScoreFragment
no muestra el resultado. Quieres que un ViewModel
retenga la puntuación para que se muestre en el ScoreFragment
. Con el patrón de método de fábrica, pasarás el valor de puntuación durante la inicialización de ViewModel
.
El patrón de método de fábrica es un patrón de diseño creativo que usa métodos de fábrica para crear objetos. Un método de fábrica es un método que muestra una instancia de la misma clase.
En esta tarea, crearás un ViewModel
con un constructor parametrizado para el fragmento de puntuación y un método de fábrica a fin de crear una instancia de ViewModel
.
- En el paquete
score
, crea una nueva clase de Kotlin llamadaScoreViewModel
. Esta clase será elViewModel
del fragmento de puntuación. - Extiende la clase
ScoreViewModel
desdeViewModel.
Agrega un parámetro de constructor para la puntuación final. Agrega un bloqueinit
con una instrucción de registro. - En la clase
ScoreViewModel
, agrega una variable llamadascore
para guardar la puntuación final.
class ScoreViewModel(finalScore: Int) : ViewModel() {
// The final score
var score = finalScore
init {
Log.i("ScoreViewModel", "Final score is $finalScore")
}
}
- En el paquete
score
, crea otra clase de Kotlin llamadaScoreViewModelFactory
. Esta clase se encargará de crear las instancias del objetoScoreViewModel
. - Extiende la clase
ScoreViewModelFactory
desdeViewModelProvider.Factory
. Agrega un parámetro de constructor para la puntuación final.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
- En
ScoreViewModelFactory
, Android Studio muestra un error sobre un miembro abstracto no implementado. Para resolver el error, anula el métodocreate()
. En el métodocreate()
, muestra el objetoScoreViewModel
recién construido.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
return ScoreViewModel(finalScore) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
- En
ScoreFragment
, crea variables de clase paraScoreViewModel
yScoreViewModelFactory
.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
- En
ScoreFragment
, dentro deonCreateView()
, después de inicializar la variablebinding
, inicializa elviewModelFactory
. Usa elScoreViewModelFactory
Pasa la puntuación final del paquete de argumentos, como un parámetro de constructor paraScoreViewModelFactory()
.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
- En
onCreateView(
, después de inicializarviewModelFactory
, inicializa el objetoviewModel
. Llama al métodoViewModelProviders.of()
, pasa el contexto del fragmento de puntuación asociado yviewModelFactory
. Esta acción creará el objetoScoreViewModel
con el método de fábrica definido en la claseviewModelFactory
.
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
- En el método
onCreateView()
, después de inicializarviewModel
, establece el texto de la vistascoreText
en la puntuación final definida enScoreViewModel
.
binding.scoreText.text = viewModel.score.toString()
- Ejecuta la app y juega. Desplázate por algunas o todas las palabras y presiona Finalizar el juego. Observa que el fragmento de puntuación ahora muestra la puntuación final.
- Opcional: Verifica los registros
ScoreViewModel
en Logcat mediante el filtrado enScoreViewModel
. Se debe mostrar el valor de la puntuación.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15
En esta tarea, implementaste ScoreFragment
para usar ViewModel
. También aprendiste a crear un constructor parametrizado para una ViewModel
mediante la interfaz ViewModelFactory
.
¡Felicitaciones! Cambiaste la arquitectura de tu app para usar uno de los componentes de la arquitectura de Android, ViewModel
. Se resolvió el problema del ciclo de vida de la app y, ahora, los datos del juego permanecen vigentes tras los cambios de configuración. También aprendiste a crear un constructor parametrizado para crear una ViewModel
mediante la interfaz ViewModelFactory
.
Proyecto de Android Studio: GuessTheWord
- Los lineamientos de arquitectura de apps de Android recomiendan separar las clases que tienen responsabilidades diferentes.
- Un controlador de IU es una clase basada en IU como
Activity
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 contener datos para mostrar en la IU. Coloca esos datos 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 de la arquitectura de Android recomendados.ViewModelProvider.Factory
es una interfaz que puedes usar para crear un objetoViewModel
.
En la siguiente tabla, se comparan los controladores de IU con las instancias de ViewModel
que contienen datos:
Controlador de IU | ViewModel |
Un ejemplo de un controlador de IU es el | Un ejemplo de un objeto |
No contiene datos para mostrar en la IU. | Contiene datos que el controlador de IU muestra en la IU. |
Contiene código para mostrar datos y código de eventos del usuario, como los objetos de escucha de clics. | Contiene el código para el procesamiento de datos. |
Se destruye y se vuelve a crear durante cada cambio de configuración. | Se destruye solo cuando el controlador de IU asociado desaparece de forma permanente, para una actividad, cuando la actividad finaliza o para un fragmento, cuando se desconecta el fragmento. |
Contiene vistas. | Nunca debería contener referencias a actividades, fragmentos o vistas, ya que no sobreviven a los cambios de configuración, pero la |
Contiene una referencia al | No contiene ninguna referencia al controlador de IU asociado. |
Curso de Udacity:
Documentación para desarrolladores de Android:
- Descripción general de ViewModel
- Cómo manejar ciclos de vida con componentes optimizados para ciclos de vida
- Guía de arquitectura de apps
ViewModelProvider
ViewModelProvider.Factory
Otro:
- Patrón arquitectónico MVVM (model-view-viewmodel).
- Principio de diseño de separación de problemas (SoC)
- Patrón de método de fábrica
En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna la tarea.
- Informa a los alumnos cómo enviar los deberes.
- Califica las tareas.
Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.
Responde estas preguntas
Pregunta 1
Para evitar perder datos durante un cambio en la configuración del dispositivo, ¿en qué clase debes guardar los datos de app?
ViewModel
LiveData
Fragment
Activity
Pregunta 2
Una ViewModel
nunca debe contener referencias a fragmentos, actividades o vistas. ¿Verdadero o falso?
- Verdadero
- Falso
Pregunta 3
¿Cuándo se destruye un ViewModel
?
- Cuando se destruye el controlador de IU asociado y se vuelve a crear durante un cambio de orientación del dispositivo.
- En un cambio de orientación.
- Cuando finaliza el controlador de IU asociado (si es una actividad) o se desconecta (si es un fragmento).
- Cuando el usuario presiona el botón Atrás.
Pregunta 4
¿Para qué sirve la interfaz ViewModelFactory
?
- Crea una instancia de un objeto
ViewModel
. - Retención de datos durante los cambios de orientación.
- Actualizando los datos que se muestran en la pantalla.
- Recibir notificaciones cuando se modifican los datos de la app.
Comienza la siguiente lección:
Para ver vínculos a otros codelabs de este curso, consulta la página de destino de codelabs sobre aspectos básicos de Kotlin para Android.