Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.
Pantalla de título | Pantalla del juego | Pantalla de puntuación |
Introducción
En este codelab, aprenderás sobre uno de los componentes de la arquitectura de Android, ViewModel
:
- Usas la clase
ViewModel
para almacenar y administrar datos relacionados con la IU de manera optimizada para los ciclos de vida. La claseViewModel
permite que los datos sobrevivan a cambios en la configuración del dispositivo, como las rotaciones de pantalla y los cambios en la disponibilidad del teclado. - Usas la clase
ViewModelFactory
para crear una instancia del objetoViewModel
y devolverlo, de modo que sobreviva a los cambios de configuración.
Conocimientos que ya deberías tener
- Cómo crear apps básicas para Android en Kotlin
- Cómo usar el gráfico de navegación para implementar la navegación en tu app
- Cómo agregar código para navegar entre los destinos de tu app y pasar datos entre los destinos de navegación
- Cómo funcionan los ciclos de vida de actividades y fragmentos
- Cómo agregar información de registro a una app y leer registros con Logcat en Android Studio
Qué aprenderás
- Cómo usar la arquitectura de apps recomendada para Android
- Cómo usar las clases
Lifecycle
,ViewModel
yViewModelFactory
en tu app - Cómo retener datos de la IU a través de cambios en la configuración del dispositivo
- 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
ViewModel
a la app para guardar sus datos de modo que sobrevivan a los cambios de configuración. - Usa
ViewModelFactory
y el patrón de diseño de método de fábrica para crear una instancia de un objetoViewModel
con parámetros de constructor.
En los codelabs de la lección 5, desarrollarás la app de GuessTheWord, comenzando con el código de inicio. GuessTheWord es un juego de adivinanzas para dos jugadores en el que ambos colaboran para obtener la puntuación más alta posible.
El primer jugador mira las palabras en la app y representa cada una por turnos, asegurándose de no mostrarle la palabra al segundo jugador. El segundo jugador intenta adivinar la palabra.
Para jugar, el primer jugador abre la app en el dispositivo y ve una palabra, por ejemplo, "guitarra", como se muestra en la siguiente captura de pantalla.
El primer jugador representa la palabra, teniendo cuidado de no decirla.
- Cuando el segundo jugador adivina la palabra correctamente, el primer jugador presiona el botón Entendido, lo que aumenta el recuento en uno y muestra la siguiente palabra.
- Si el segundo jugador no puede adivinar la palabra, el primer jugador presiona el botón Omitir, lo que disminuye el recuento en uno y pasa a la siguiente palabra.
- Para finalizar el juego, presiona el botón Finalizar juego. (Esta funcionalidad no se encuentra en el código de partida del primer codelab de la serie).
En esta tarea, descargarás y ejecutarás la app inicial, y examinarás el código.
Paso 1: Inicio
- 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 siguiente palabra y disminuye la puntuación en uno, y el botón Entendido muestra la siguiente palabra y aumenta la puntuación en uno. El botón End Game no está implementado, por lo que no sucede nada cuando lo presionas.
Paso 2: Realiza una revisión del código
- En Android Studio, explora el código para comprender cómo funciona la app.
- Asegúrate de consultar los archivos que se describen a continuación, ya que son particularmente importantes.
MainActivity.kt
Este archivo solo contiene código predeterminado generado por plantillas.
res/layout/main_activity.xml
Este archivo contiene el diseño principal de la app. El NavHostFragment
aloja los otros fragmentos a medida que el usuario navega por la app.
Fragmentos de la IU
El código de partida tiene tres fragmentos en tres paquetes diferentes en el paquete com.example.android.guesstheword.screens
:
title/TitleFragment
para la pantalla de títulogame/GameFragment
para la pantalla del juegoscore/ScoreFragment
para la pantalla de puntuación
screens/title/TitleFragment.kt
El fragmento de título es la primera pantalla que se muestra cuando se inicia la app. Se establece un controlador de clics en el botón Play para navegar a la pantalla del juego.
screens/game/GameFragment.kt
Este es el fragmento principal, en el que se produce la mayor parte de la acción del juego:
- Las variables se definen para la palabra actual y la puntuación actual.
- El
wordList
definido dentro del métodoresetList()
es una lista de muestra de palabras que se usarán en el juego. - El método
onSkip()
es el controlador de clics del botón Omitir. Disminuye la puntuación en 1 y, luego, muestra la siguiente palabra con el métodonextWord()
. - El método
onCorrect()
es el controlador de clics del botón Entendido. 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 restarle.
screens/score/ScoreFragment.kt
ScoreFragment
es la pantalla final del juego y muestra la puntuación final del jugador. En este codelab, agregarás la implementación para mostrar esta pantalla y el puntaje final.
res/navigation/main_navigation.xml
El gráfico de navegación muestra cómo se conectan los fragmentos a través de la navegación:
- Desde el fragmento del título, el usuario puede navegar al fragmento del juego.
- Desde el fragmento del juego, el usuario puede navegar al fragmento de la puntuación.
- Desde el fragmento de puntuación, el usuario puede volver al fragmento del juego.
En esta tarea, encontrarás problemas con la app de inicio de GuessTheWord.
- Ejecuta el código de partida y juega con algunas palabras. Presiona Skip o Got It después de cada palabra.
- Ahora, la pantalla del juego muestra una palabra y la puntuación actual. Cambia la orientación de la pantalla girando el dispositivo o el emulador. Observa que se pierde la puntuación actual.
- Juega con algunas palabras más. Cuando se muestre la pantalla del juego con una puntuación, cierra y vuelve a abrir la app. Observa que el juego se reinicia desde el principio, ya que no se guarda el estado de la app.
- Juega con algunas palabras y, luego, presiona el botón End Game. Observa que no sucede nada.
Problemas en la app:
- La app de partida no guarda ni restablece el estado de la app durante los cambios de configuración, como cuando cambia la orientación del dispositivo o cuando la app se cierra y se reinicia.
Puedes resolver este problema con la devolución de llamadaonSaveInstanceState()
. Sin embargo, para usar el métodoonSaveInstanceState()
debes escribir código adicional para guardar el estado en un paquete y, luego, implementar la lógica para recuperar ese estado. Además, la cantidad de datos que se pueden almacenar es mínima. - La pantalla del juego no navega a la pantalla de puntuación cuando el usuario presiona el botón End Game.
Puedes resolver estos problemas usando los componentes de la arquitectura de la app que aprenderás en este codelab.
Arquitectura de la app
La arquitectura de la app es una forma de diseñar las clases de tus apps y las relaciones entre ellas, de modo que el código esté organizado, funcione bien en situaciones particulares y sea fácil de usar. En este conjunto de cuatro codelabs, las mejoras que realices en la app de GuessTheWord seguirán los lineamientos de la arquitectura de apps para Android y usarás los componentes de arquitectura de Android. La arquitectura de la app para Android es similar al patrón arquitectónico MVVM (model-view-viewmodel).
La app de GuessTheWord sigue el principio de diseño de separación de problemas y se divide en clases, en las que cada clase aborda un problema independiente. En este primer codelab de la lección, las clases con las que trabajarás son un controlador de IU, un ViewModel
y un ViewModelFactory
.
Controlador de IU
Un controlador de IU es una clase basada en la IU, como Activity
o Fragment
. Un controlador de IU solo debería contener lógica que se ocupe de interacciones del sistema operativo y de IU, como mostrar vistas y capturar la entrada del usuario. No coloques lógica de toma de decisiones, como la lógica que determina el texto que se mostrará, en el controlador de IU.
En el código de inicio de GuessTheWord, los controladores de IU son los tres fragmentos: GameFragment
, ScoreFragment,
y TitleFragment
. Según el principio de diseño de "separación de responsabilidades", el GameFragment
solo es responsable de dibujar los elementos del juego en la pantalla y saber cuándo el usuario presiona los botones, y nada más. Cuando el usuario presiona un botón, esta información se pasa a GameViewModel
.
ViewModel
Un ViewModel
contiene datos que se mostrarán en un fragmento o una actividad asociados con el ViewModel
. Un ViewModel
puede hacer cálculos y transformaciones simples en los datos para prepararlos para que el controlador de la IU los muestre. En esta arquitectura, ViewModel
toma las decisiones.GameViewModel
contiene datos como el valor de la puntuación, la lista de palabras y la palabra actual, ya que estos son los datos que se mostrarán en la pantalla. El GameViewModel
también contiene la lógica empresarial para realizar cálculos simples y decidir cuál es el estado actual de los datos.
ViewModelFactory
Un ViewModelFactory
crea instancias de objetos ViewModel
, con o sin parámetros de constructor.
En codelabs posteriores, aprenderás sobre otros componentes de la arquitectura de Android relacionados con los controladores de la IU y ViewModel
.
La clase ViewModel
está diseñada para almacenar y administrar los datos relacionados con la IU. En esta app, cada ViewModel
se asocia con un fragmento.
En esta tarea, agregarás tu primer ViewModel
a la app, el GameViewModel
para el GameFragment
. También aprenderás qué significa que ViewModel
esté optimizado para los ciclos de vida.
Paso 1: Agrega la clase GameViewModel
- 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, la app de solución debería compilarse según lo esperado. Si no es así, intenta resolver el problema o revierte a la versión que se muestra a continuación.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
- En la carpeta del paquete
screens/game/
, crea una nueva clase de Kotlin llamadaGameViewModel
. - Haz que la clase
GameViewModel
extienda la clase abstractaViewModel
. - Para ayudarte a comprender mejor cómo
ViewModel
tiene en cuenta el ciclo de vida, agrega un bloqueinit
con una instrucciónlog
.
class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
Paso 2: Anula onCleared() y agrega registros
El objeto ViewModel
se destruye cuando se desconecta el fragmento asociado o cuando finaliza la actividad. Justo antes de que se destruya ViewModel
, se llama a la devolución de llamada onCleared()
para limpiar los recursos.
- 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 del juego
Se debe asociar un ViewModel
con un controlador de IU. Para asociar los dos, crea una referencia al ViewModel
dentro del controlador de IU.
En este paso, crearás una referencia de GameViewModel
dentro del controlador de IU correspondiente, que es GameFragment
.
- 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, las instancias de ViewModel
sobreviven. Si creas la instancia de ViewModel
con la clase ViewModel
, se creará un objeto nuevo cada vez que se vuelva a crear el fragmento. En su lugar, crea la instancia de ViewModel
con un ViewModelProvider
.
Cómo funciona ViewModelProvider
:
ViewModelProvider
devuelve unViewModel
existente si hay uno, o crea uno nuevo si aún no existe.ViewModelProvider
crea una instancia deViewModel
en asociación con el alcance determinado (una actividad o un fragmento).- El
ViewModel
creado se retiene mientras el alcance esté activo. Por ejemplo, si el alcance es un fragmento, elViewModel
se conserva hasta que se desconecta el fragmento.
Inicializa 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
. - Sobre 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 de Logcat y filtra en
Game
. Presiona el botón Reproducir en tu dispositivo o emulador. Se abre la pantalla del juego.
Como se muestra en Logcat, el métodoonCreateView()
deGameFragment
llama al métodoViewModelProviders.of()
para crear elGameViewModel
. Las instrucciones de registro que agregaste aGameFragment
yGameViewModel
aparecen en 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. Pero 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 navega fuera del fragmento del 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!
El ViewModel
sobrevive a los cambios de configuración, por lo que es un buen lugar para los datos que deben sobrevivir a los cambios de configuración:
- Coloca los datos que se mostrarán en la pantalla y el código para procesarlos en el objeto
ViewModel
. - El
ViewModel
nunca debe contener referencias a fragmentos, actividades o vistas, ya que estos no sobreviven a los cambios de configuración.
Para comparar, a continuación, se muestra cómo se controlan los datos de la IU de GameFragment
en la app de inicio antes de agregar ViewModel
y después de agregar ViewModel
:
- Antes de agregar
ViewModel
:
Cuando la app pasa por un cambio de configuración, como una rotación de pantalla, el fragmento del juego se destruye y se vuelve a crear. Se pierden los datos. - Después de agregar
ViewModel
y mover los datos de la IU del fragmento del juego aViewModel
:
Todos los datos que el fragmento necesita mostrar ahora sonViewModel
. Cuando la app pasa por un cambio de configuración, elViewModel
sobrevive y los datos se conservan.
En esta tarea, moverás los datos de la IU de la app a la clase GameViewModel
, junto con los métodos para procesar los datos. Esto se hace para que los datos se conserven durante los cambios de configuración.
Paso 1: Mueve los campos de datos y el procesamiento de datos al ViewModel
Mueve los siguientes campos y métodos de datos de GameFragment
a GameViewModel
:
- Mueve los campos de datos
word
,score
ywordList
. Asegúrate de queword
yscore
no seanprivate
.
No muevas la variable de vinculación,GameFragmentBinding
, porque contiene referencias a las vistas. Esta variable se usa para expandir el diseño, configurar los objetos de escucha de clics y mostrar los datos en la pantalla, responsabilidades del fragmento. - Mueve los métodos
resetList()
ynextWord()
. Estos métodos deciden qué palabra mostrar en la pantalla. - Desde dentro del método
onCreateView()
, mueve las llamadas a los métodosresetList()
ynextWord()
al bloqueinit
deGameViewModel
.
Estos métodos deben estar en el bloqueinit
, ya que debes restablecer la lista de palabras cuando se creaViewModel
, 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 debe moverse al 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 de la clase GameViewModel
después de la refactorización:
class GameViewModel : ViewModel() {
// The current word
var word = ""
// The current score
var score = 0
// The list of words - the front of the list is the next word to guess
private lateinit var wordList: MutableList<String>
/**
* Resets the list of words and randomizes the order
*/
private fun resetList() {
wordList = mutableListOf(
"queen",
"hospital",
"basketball",
"cat",
"change",
"snail",
"soup",
"calendar",
"sad",
"desk",
"guitar",
"home",
"railway",
"zebra",
"jelly",
"car",
"crow",
"trade",
"bag",
"roll",
"bubble"
)
wordList.shuffle()
}
init {
resetList()
nextWord()
Log.i("GameViewModel", "GameViewModel created!")
}
/**
* Moves to the next word in the list
*/
private fun nextWord() {
if (!wordList.isEmpty()) {
//Select and remove a word from the list
word = wordList.removeAt(0)
}
updateWordText()
updateScoreText()
}
/** Methods for buttons presses **/
fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
}
Este es el código de la clase GameFragment
después de la refactorización:
/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {
private lateinit var binding: GameFragmentBinding
private lateinit var viewModel: GameViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate view and obtain an instance of the binding class
binding = DataBindingUtil.inflate(
inflater,
R.layout.game_fragment,
container,
false
)
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
updateScoreText()
updateWordText()
return binding.root
}
/** Methods for button click handlers **/
private fun onSkip() {
if (!wordList.isEmpty()) {
score--
}
nextWord()
}
private fun onCorrect() {
if (!wordList.isEmpty()) {
score++
}
nextWord()
}
/** Methods for updating the UI **/
private fun updateWordText() {
binding.wordText.text = word
}
private fun updateScoreText() {
binding.scoreText.text = score.toString()
}
}
Paso 2: Actualiza las referencias a los controladores de clics y los campos de datos en GameFragment
- 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 que usen las variablesGameViewModel
, ya que ahora se encuentran 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 desdeGameFragment
. - 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. Mientras estás en la pantalla del juego, gira el dispositivo. Observa que la puntuación y la palabra actuales se conservan después del cambio de orientación.
¡Bien hecho! Ahora todos los datos de tu app se almacenan en un ViewModel
, por lo que se conservan durante los cambios de configuración.
En esta tarea, implementarás el objeto de escucha de clics para el botón End Game.
- 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 Entendido y Omitir. Justo debajo de estas dos líneas, configura un objeto de escucha de clics para el botón End Game. Usa la variable de vinculación,binding
. Dentro del objeto de escucha de clics, llama al métodoonEndGame()
.
binding.endGameButton.setOnClickListener { onEndGame() }
- En
GameFragment
, agrega un método llamadogameFinished()
para navegar por la app hasta la pantalla de puntuación. Pasa la puntuación como un argumento con Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
val action = GameFragmentDirections.actionGameToScore()
action.score = viewModel.score
NavHostFragment.findNavController(this).navigate(action)
}
- En el método
onEndGame()
, llama al métodogameFinished()
.
private fun onEndGame() {
gameFinished()
}
- Ejecuta la app, juega y prueba algunas palabras. Presiona el botón Finalizar juego . Observa que la app navega a la pantalla de puntuación, pero no se muestra la puntuación final. Corregirás esto en la siguiente tarea.
Cuando el usuario finaliza el juego, el ScoreFragment
no muestra la puntuación. Quieres que un ViewModel
contenga la puntuación que mostrará el ScoreFragment
. Pasarás el valor de la puntuación durante la inicialización de ViewModel
con el patrón de método de fábrica.
El patrón de método de fábrica es un patrón de diseño creacional que usa métodos de fábrica para crear objetos. Un método de fábrica es un método que devuelve una instancia de la misma clase.
En esta tarea, crearás un ViewModel
con un constructor parametrizado para el fragmento de puntuación y un método de fábrica para crear una instancia del ViewModel
.
- En el paquete
score
, crea una nueva clase de Kotlin llamadaScoreViewModel
. Esta clase será elViewModel
para el fragmento de puntuación. - Extiende la clase
ScoreViewModel
deViewModel.
. 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 será responsable de crear una instancia 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()
, devuelve 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
, inicializaviewModelFactory
. UsaScoreViewModelFactory
. Pasa la puntuación final del paquete de argumentos como un parámetro del constructor aScoreViewModelFactory()
.
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
. Esto 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. Recorre algunas palabras o todas y presiona Finalizar juego. Observa que el fragmento de la puntuación ahora muestra la puntuación final.
- Opcional: Verifica los registros de
ScoreViewModel
en Logcat filtrando porScoreViewModel
. Se debe mostrar el valor de la puntuación.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15
En esta tarea, implementaste ScoreFragment
para usar ViewModel
. También aprendiste a crear un constructor parametrizado para un ViewModel
con la interfaz ViewModelFactory
.
¡Felicitaciones! Cambiaste la arquitectura de tu app para usar uno de los componentes de la arquitectura de Android, ViewModel
. Resolviste el problema del ciclo de vida de la app y, ahora, los datos del juego sobreviven a los cambios de configuración. También aprendiste a crear un constructor con parámetros para crear un ViewModel
, usando la interfaz ViewModelFactory
.
Proyecto de Android Studio: GuessTheWord
- Los lineamientos de arquitectura de apps de Android recomiendan separar las clases que tienen responsabilidades diferentes.
- Un controlador de IU es una clase basada en la IU, como
Activity
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 los datos sobrevivan a cambios de configuración, como las rotaciones de pantalla. ViewModel
es uno de los componentes de la arquitectura de Android recomendados.ViewModelProvider.Factory
es una interfaz que puedes usar para crear un objetoViewModel
.
En la siguiente tabla, se comparan los controladores de la IU con las instancias de ViewModel
que contienen datos para ellos:
Controlador de IU | ViewModel |
Un ejemplo de controlador de IU es el | Un ejemplo de |
No contiene datos para mostrar en la IU. | Contiene datos que el controlador de IU muestra en la IU. |
Contiene código para mostrar datos y código de eventos del usuario, como objetos de escucha de clics. | Contiene código para el procesamiento de datos. |
Se destruye y se vuelve a crear durante cada cambio de configuración. | Se destruye solo cuando el controlador de IU asociado desaparece de forma permanente (en el caso de una actividad, cuando finaliza; en el caso de un fragmento, cuando se desconecta). |
Contiene vistas. | Nunca debe contener referencias a actividades, fragmentos o vistas, ya que no sobreviven a los cambios de configuración, pero el |
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 (modelo-vista-modelo de vista).
- 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 actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna una tarea.
- Comunicarles a los alumnos cómo enviar las actividades para el hogar.
- Califica las actividades para el hogar.
Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.
Responde estas preguntas:
Pregunta 1
Para evitar perder datos durante un cambio en la configuración del dispositivo, ¿en qué clase deberías guardar los datos de apps?
ViewModel
LiveData
Fragment
Activity
Pregunta 2
Un ViewModel
nunca debe contener referencias a fragmentos, actividades o vistas. ¿Verdadero o falso?
- Verdadero
- Falso
Pregunta 3
¿Cuándo se destruye ViewModel
?
- Cuando se destruye el controlador de IU asociado y se vuelve a crear durante un cambio de orientación del dispositivo.
- En un cambio de orientación.
- Cuando finaliza el controlador de IU asociado (si es una actividad) o se desconecta (si es un fragmento).
- Cuando el usuario presiona el botón Atrás.
Pregunta 4
¿Para qué sirve la interfaz ViewModelFactory
?
- Creando una instancia del objeto
ViewModel
. - Conservando los datos durante los cambios de orientación.
- Actualizando los datos que se muestran en pantalla.
- Recibiendo notificaciones cuando cambian los datos de app.
Comienza la siguiente lección:
Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.