Conceitos básicos do Kotlin para Android 05.1: ViewModel e ViewModelFactory

Este codelab faz parte do curso Conceitos básicos do Kotlin para Android. Você vai aproveitar mais este curso se fizer os codelabs em sequência. Todos os codelabs do curso estão listados na página inicial dos codelabs de princípios básicos do Kotlin para Android.

Créditos

Tela do jogo

Tela de pontuação

Introdução

Neste codelab, você vai aprender sobre um dos componentes da arquitetura do Android, o ViewModel:

  • Você usa a classe ViewModel para armazenar e gerenciar dados relacionados à interface considerando o ciclo de vida. A classe ViewModel permite que os dados sobrevivam a mudanças na configuração do dispositivo, como rotações de tela e mudanças na disponibilidade do teclado.
  • Use a classe ViewModelFactory para instanciar e retornar o objeto ViewModel que sobrevive a mudanças de configuração.

O que você já precisa saber

  • Como criar apps Android básicos em Kotlin.
  • Como usar o gráfico de navegação para implementar a navegação no app.
  • Como adicionar código para navegar entre os destinos do app e transmitir dados entre eles.
  • Como os ciclos de vida de atividades e fragmentos funcionam.
  • Como adicionar informações de registro a um app e ler registros usando o Logcat no Android Studio.

O que você vai aprender

Atividades deste laboratório

  • Adicione um ViewModel ao app para salvar os dados dele e garantir que eles sobrevivam às mudanças de configuração.
  • Use ViewModelFactory e o padrão de projeto de método de fábrica para instanciar um objeto ViewModel com parâmetros de construtor.

Nos codelabs da lição 5, você vai desenvolver o app GuessTheWord, começando com o código inicial. O GuessTheWord é um jogo de charadas para dois jogadores em que eles colaboram para conseguir a maior pontuação possível.

O primeiro jogador olha as palavras no app e representa cada uma delas, sem mostrar a palavra para o segundo jogador. O segundo jogador tenta adivinhar a palavra.

Para jogar, o primeiro jogador abre o app no dispositivo e vê uma palavra, por exemplo, "guitarra", como mostrado na captura de tela abaixo.

O primeiro jogador representa a palavra, tomando cuidado para não dizer a palavra em si.

  • Quando o segundo jogador acerta a palavra, o primeiro pressiona o botão Entendi, que aumenta a contagem em um e mostra a próxima palavra.
  • Se o segundo jogador não conseguir adivinhar a palavra, o primeiro jogador pressiona o botão Pular, que diminui a contagem em um e pula para a próxima palavra.
  • Para encerrar o jogo, pressione o botão Encerrar jogo. Essa funcionalidade não está no código inicial do primeiro codelab da série.

Nesta tarefa, você vai baixar e executar o app inicial e examinar o código.

Etapa 1: primeiros passos

  1. Faça o download do código inicial do GuessTheWord (link em inglês) e abra o projeto no Android Studio.
  2. Execute o app em um dispositivo Android ou em um emulador.
  3. Toque nos botões. O botão Pular mostra a próxima palavra e diminui a pontuação em um, e o botão Entendi mostra a próxima palavra e aumenta a pontuação em um. O botão Encerrar jogo não está implementado, então nada acontece quando você toca nele.

Etapa 2: fazer um tutorial de código

  1. No Android Studio, analise o código para entender como o app funciona.
  2. Confira os arquivos descritos abaixo, que são particularmente importantes.

MainActivity.kt

Esse arquivo contém apenas o código padrão gerado pelo modelo.

res/layout/main_activity.xml

Esse arquivo contém o layout principal do app. O NavHostFragment hospeda os outros fragmentos à medida que o usuário navega pelo app.

Fragmentos de interface

O código inicial tem três fragmentos em três pacotes diferentes no pacote com.example.android.guesstheword.screens:

  • title/TitleFragment para a tela de título
  • game/GameFragment para a tela do jogo
  • score/ScoreFragment para a tela de pontuação

screens/title/TitleFragment.kt

O fragmento de título é a primeira tela exibida quando o app é iniciado. Um gerenciador de cliques é definido para o botão Jogar, para navegar até a tela do jogo.

screens/game/GameFragment.kt

Esse é o fragmento principal, onde a maior parte da ação do jogo acontece:

  • As variáveis são definidas para a palavra atual e a pontuação atual.
  • O wordList definido no método resetList() é uma lista de exemplo de palavras a serem usadas no jogo.
  • O método onSkip() é o gerenciador de cliques do botão Pular. Ele diminui a pontuação em 1 e mostra a próxima palavra usando o método nextWord().
  • O método onCorrect() é o gerenciador de cliques do botão Entendi. Esse método é implementado de maneira semelhante ao método onSkip(). A única diferença é que esse método adiciona 1 à pontuação em vez de subtrair.

screens/score/ScoreFragment.kt

ScoreFragment é a tela final do jogo e mostra a pontuação final do jogador. Neste codelab, você vai adicionar a implementação para mostrar essa tela e a pontuação final.

res/navigation/main_navigation.xml

O gráfico de navegação mostra como os fragmentos estão conectados pela navegação:

  • No fragmento de título, o usuário pode navegar até o fragmento de jogo.
  • No fragmento do jogo, o usuário pode navegar até o fragmento da pontuação.
  • No fragmento de pontuação, o usuário pode voltar ao fragmento do jogo.

Nesta tarefa, você vai encontrar problemas com o app inicial GuessTheWord.

  1. Execute o código inicial e jogue uma partida com algumas palavras, tocando em Pular ou Entendi após cada palavra.
  2. A tela do jogo agora mostra uma palavra e a pontuação atual. Gire o dispositivo ou emulador para mudar a orientação da tela. A pontuação atual é perdida.
  3. Jogue com mais algumas palavras. Quando a tela do jogo aparecer com uma pontuação, feche e abra o app novamente. O jogo vai reiniciar do começo porque o estado do app não foi salvo.
  4. Jogue uma partida com algumas palavras e toque no botão Encerrar jogo. Nada acontece.

Problemas no app:

  • O app inicial não salva e restaura o estado dele durante mudanças de configuração, como quando a orientação do dispositivo muda ou quando o app é desligado e reiniciado.
    Você pode resolver esse problema usando o callback onSaveInstanceState(). No entanto, o uso do método onSaveInstanceState() exige que você escreva um código extra para salvar o estado em um pacote e implementar a lógica para acessar esse estado. Além disso, a quantidade de dados que pode ser armazenada é mínima.
  • A tela do jogo não navega para a tela de pontuação quando o usuário toca no botão Encerrar jogo.

É possível resolver esses problemas usando os componentes da arquitetura do app que você vai aprender neste codelab.

Arquitetura de apps

A arquitetura de apps é uma maneira de projetar as classes dos apps e as relações entre elas para que o código seja organizado, tenha bom desempenho em cenários específicos e seja fácil de trabalhar. Neste conjunto de quatro codelabs, as melhorias feitas no app GuessTheWord seguem as diretrizes da arquitetura de apps Android, e você usa os Componentes de arquitetura do Android. A arquitetura de apps Android é semelhante ao padrão arquitetônico MVVM (model-view-viewmodel).

O app GuessTheWord segue o princípio de design de separação de conceitos e é dividido em classes, cada uma abordando um conceito separado. Neste primeiro codelab da lição, as classes com que você vai trabalhar são um controlador de interface, um ViewModel e um ViewModelFactory.

Controlador de interface

Um controlador de UI é uma classe baseada em UI, como Activity ou Fragment. Um controlador de UI precisa conter apenas a lógica que processa as interações entre a UI e o sistema operacional, como mostrar visualizações e capturar entradas do usuário. Não coloque lógica de tomada de decisão, como a que determina o texto a ser exibido, no controlador de UI.

No código inicial do GuessTheWord, os controladores de UI são os três fragmentos: GameFragment, ScoreFragment, e TitleFragment. Seguindo o princípio de design de "separação de responsabilidades", o GameFragment é responsável apenas por desenhar elementos do jogo na tela e saber quando o usuário toca nos botões, e nada mais. Quando o usuário toca em um botão, essas informações são transmitidas ao GameViewModel.

ViewModel

Um ViewModel contém dados que serão mostrados em um fragmento ou uma atividade associada ao ViewModel. Um ViewModel pode fazer cálculos e transformações simples nos dados para preparar os dados a serem exibidos pelo controlador da interface. Nessa arquitetura, o ViewModel toma as decisões.

O GameViewModel contém dados como o valor da pontuação, a lista de palavras e a palavra atual, porque são os dados que serão mostrados na tela. O GameViewModel também contém a lógica de negócios para realizar cálculos simples e decidir qual é o estado atual dos dados.

ViewModelFactory

Um ViewModelFactory cria instâncias de objetos ViewModel, com ou sem parâmetros de construtor.

Em codelabs futuros, você vai aprender sobre outros componentes da arquitetura do Android relacionados a controladores de interface e ViewModel.

A classe ViewModel foi projetada para armazenar e gerenciar os dados relacionados à interface. Neste app, cada ViewModel está associado a um fragmento.

Nesta tarefa, você vai adicionar o primeiro ViewModel ao app, o GameViewModel do GameFragment. Você também vai aprender o que significa o fato de ViewModel ser compatível com o ciclo de vida.

Etapa 1: adicionar a classe GameViewModel

  1. Abra o arquivo build.gradle(module:app). No bloco dependencies, adicione a dependência do Gradle para ViewModel.

    . Se você usar a versão mais recente da biblioteca, o app de solução será compilado conforme o esperado. Se não for, tente resolver o problema ou volte para a versão mostrada abaixo.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. Na pasta do pacote screens/game/, crie uma classe Kotlin chamada GameViewModel.
  2. Faça a classe GameViewModel estender a classe abstrata ViewModel.
  3. Para ajudar você a entender melhor como o ViewModel reconhece o ciclo de vida, adicione um bloco init com uma instrução log.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

Etapa 2: substituir onCleared() e adicionar registros

O ViewModel é destruído quando o fragmento associado é desanexado ou quando a atividade é concluída. Logo antes do ViewModel ser destruído, o callback onCleared() é chamado para limpar os recursos.

  1. Na classe GameViewModel, substitua o método onCleared().
  2. Adicione um log statement ao método onCleared() para monitorar o ciclo de vida do GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Etapa 3: associar GameViewModel ao fragmento do jogo

Um ViewModel precisa estar associado a um controlador de UI. Para associar os dois, crie uma referência ao ViewModel no controlador de UI.

Nesta etapa, você vai criar uma referência do GameViewModel no controlador de UI correspondente, que é o GameFragment.

  1. Na classe GameFragment, adicione um campo do tipo GameViewModel no nível superior como uma variável de classe.
private lateinit var viewModel: GameViewModel

Etapa 4: inicializar o ViewModel

Durante mudanças de configuração, como rotações de tela, os controladores de UI, como fragmentos, são recriados. No entanto, as instâncias de ViewModel sobrevivem. Se você criar a instância ViewModel usando a classe ViewModel, um novo objeto será criado sempre que o fragmento for recriado. Em vez disso, crie a instância ViewModel usando um ViewModelProvider.

Como o ViewModelProvider funciona:

  • O ViewModelProvider retorna um ViewModel existente, se houver, ou cria um novo, se ele ainda não existir.
  • O ViewModelProvider cria uma instância ViewModel em associação com o escopo especificado (uma atividade ou um fragmento).
  • O ViewModel criado é mantido enquanto o escopo estiver ativo. Por exemplo, se o escopo for um fragmento, o ViewModel será mantido até que o fragmento seja desanexado.

Inicialize o ViewModel usando o método ViewModelProviders.of() para criar um ViewModelProvider:

  1. Na classe GameFragment, inicialize a variável viewModel. Coloque esse código dentro de onCreateView(), depois da definição da variável de vinculação. Use o método ViewModelProviders.of() e transmita o contexto GameFragment associado e a classe GameViewModel.
  2. Acima da inicialização do objeto ViewModel, adicione um log statement para registrar a chamada do método ViewModelProviders.of().
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. Execute o app. No Android Studio, abra o painel Logcat e filtre por Game. Toque no botão Play no dispositivo ou emulador. A tela do jogo é aberta.

    Como mostrado no Logcat, o método onCreateView() do GameFragment chama o método ViewModelProviders.of() para criar o GameViewModel. As instruções de registro que você adicionou ao GameFragment e ao GameViewModel aparecem no Logcat.

  1. Ative a configuração de giro automático do dispositivo ou emulador e mude a orientação da tela algumas vezes. O GameFragment é destruído e recriado todas as vezes, então o ViewModelProviders.of() é chamado todas as vezes. mas o GameViewModel é criado apenas uma vez e não é recriado nem destruído para cada chamada.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. Saia do jogo ou navegue para fora do fragmento do jogo. O GameFragment será destruído. O GameViewModel associado também é destruído, e o callback onCleared() é chamado.
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!

O ViewModel sobrevive a mudanças de configuração. Por isso, é um bom lugar para dados que precisam sobreviver a essas mudanças:

  • Coloque os dados a serem mostrados na tela e o código para processar esses dados no ViewModel.
  • O ViewModel nunca pode conter referências a fragmentos, atividades ou visualizações, porque eles não persistem após mudanças na configuração.

Para fins de comparação, veja como os dados da interface GameFragment são processados no app inicial antes e depois de adicionar ViewModel:ViewModel

  • Antes de adicionar ViewModel:
    Quando o app passa por uma mudança de configuração, como uma rotação de tela, o fragmento do jogo é destruído e recriado. Os dados são perdidos.
  • Depois de adicionar ViewModel e mover os dados da interface do fragmento do jogo para o ViewModel:
    . Todos os dados que o fragmento precisa mostrar agora são o ViewModel. Quando o app passa por uma mudança de configuração, o ViewModel sobrevive e os dados são mantidos.

Nesta tarefa, você vai mover os dados da interface do app para a classe GameViewModel, junto com os métodos para processar os dados. Isso é feito para que os dados sejam mantidos durante as mudanças de configuração.

Etapa 1: mover campos de dados e processamento de dados para o ViewModel

Mova os seguintes campos de dados e métodos de GameFragment para GameViewModel:

  1. Mova os campos de dados word, score e wordList. Verifique se word e score não são private.

    Não mova a variável de vinculação, GameFragmentBinding, porque ela contém referências às visualizações. Essa variável é usada para inflar o layout, configurar os listeners de clique e mostrar os dados na tela, que são responsabilidades do fragmento.
  2. Mova os métodos resetList() e nextWord(). Esses métodos decidem qual palavra mostrar na tela.
  3. No método onCreateView(), mova as chamadas de método para resetList() e nextWord() para o bloco init do GameViewModel.

    Esses métodos precisam estar no bloco init, porque você precisa redefinir a lista de palavras quando o ViewModel é criado, não sempre que o fragmento é criado. Você pode excluir a instrução de registro no bloco init de GameFragment.

Os manipuladores de cliques onSkip() e onCorrect() no GameFragment contêm código para processar os dados e atualizar a interface. O código para atualizar a interface precisa ficar no fragmento, mas o código para processar os dados precisa ser movido para o ViewModel.

Por enquanto, coloque os métodos idênticos nos dois lugares:

  1. Copie os métodos onSkip() e onCorrect() da GameFragment para a GameViewModel.
  2. No GameViewModel, verifique se os métodos onSkip() e onCorrect() não são private, porque você vai referenciar esses métodos do fragmento.

Confira o código da classe GameViewModel após a refatoração:

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!")
   }
}

Confira o código da classe GameFragment após a refatoração:

/**
* 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()
   }
}

Etapa 2: atualizar referências a manipuladores de cliques e campos de dados em GameFragment

  1. No GameFragment, atualize os métodos onSkip() e onCorrect(). Remova o código para atualizar a pontuação e chame os métodos onSkip() e onCorrect() correspondentes em viewModel.
  2. Como você moveu o método nextWord() para o ViewModel, o fragmento do jogo não pode mais acessá-lo.

    Em GameFragment, nos métodos onSkip() e onCorrect(), substitua a chamada para nextWord() por updateScoreText() e updateWordText(). Esses métodos mostram os dados na tela.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. No GameFragment, atualize as variáveis score e word para usar as variáveis GameViewModel, porque elas agora estão no GameViewModel.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. No GameViewModel, dentro do método nextWord(), remova as chamadas para os métodos updateWordText() e updateScoreText(). Esses métodos agora estão sendo chamados do GameFragment.
  2. Crie o app e verifique se não há erros. Se houver erros, limpe e recrie o projeto.
  3. Execute o app e jogue uma partida com algumas palavras. Na tela do jogo, gire o dispositivo. A pontuação e a palavra atuais são mantidas após a mudança de orientação.

Muito bem! Agora, todos os dados do app são armazenados em um ViewModel, então eles são mantidos durante as mudanças de configuração.

Nesta tarefa, você vai implementar o listener de clique para o botão End Game.

  1. Em GameFragment, adicione um método chamado onEndGame(). O método onEndGame() será chamado quando o usuário tocar no botão Encerrar jogo.
private fun onEndGame() {
   }
  1. Em GameFragment, dentro do método onCreateView(), localize o código que define listeners de clique para os botões Entendi e Pular. Logo abaixo dessas duas linhas, defina um listener de clique para o botão End Game. Use a variável de vinculação, binding. No listener de cliques, chame o método onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. Em GameFragment, adicione um método chamado gameFinished() para navegar até a tela de pontuação. Transmita a pontuação como um argumento usando o Safe Args.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. No método onEndGame(), chame o método gameFinished().
private fun onEndGame() {
   gameFinished()
}
  1. Execute o app, jogue e passe por algumas palavras. Toque no botão Encerrar jogo . O app navega até a tela de pontuação, mas a pontuação final não é mostrada. Isso será corrigido na próxima tarefa.

Quando o usuário termina o jogo, o ScoreFragment não mostra a pontuação. Você quer um ViewModel para manter a pontuação a ser mostrada pelo ScoreFragment. Você vai transmitir o valor da pontuação durante a inicialização do ViewModel usando o padrão de método de fábrica.

O padrão de método de fábrica é um padrão de projeto de criação que usa métodos de fábrica para criar objetos. Um método de fábrica é um método que retorna uma instância da mesma classe.

Nesta tarefa, você vai criar um ViewModel com um construtor parametrizado para o fragmento de pontuação e um método de fábrica para instanciar o ViewModel.

  1. No pacote score, crie uma classe Kotlin chamada ScoreViewModel. Essa classe será a ViewModel do fragmento de pontuação.
  2. Estenda a classe ScoreViewModel de ViewModel.. Adicione um parâmetro construtor para a pontuação final. Adicione um bloco init com um log statement.
  3. Na classe ScoreViewModel, adicione uma variável chamada score para salvar a pontuação final.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. No pacote score, crie outra classe Kotlin chamada ScoreViewModelFactory. Essa classe será responsável por instanciar o objeto ScoreViewModel.
  2. Estenda a classe ScoreViewModelFactory de ViewModelProvider.Factory. Adicione um parâmetro construtor para a pontuação final.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. Em ScoreViewModelFactory, o Android Studio mostra um erro sobre um membro abstrato não implementado. Para resolver o erro, substitua o método create(). No método create(), retorne o objeto ScoreViewModel recém-criado.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. Em ScoreFragment, crie variáveis de classe para ScoreViewModel e ScoreViewModelFactory.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. Em ScoreFragment, dentro de onCreateView(), depois de inicializar a variável binding, inicialize o viewModelFactory. Use a ScoreViewModelFactory. Transmita a pontuação final do pacote de argumentos como um parâmetro do construtor para o ScoreViewModelFactory().
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. Em onCreateView(, depois de inicializar viewModelFactory, inicialize o objeto viewModel. Chame o método ViewModelProviders.of(), transmita o contexto do fragmento de pontuação associado e viewModelFactory. Isso vai criar o objeto ScoreViewModel usando o método de fábrica definido na classe viewModelFactory..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. No método onCreateView(), depois de inicializar o viewModel, defina o texto da visualização scoreText como a pontuação final definida no ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Execute o app e jogue uma partida. Passe por algumas ou todas as palavras e toque em Encerrar jogo. O fragmento de pontuação agora mostra a pontuação final.

  1. Opcional: verifique os registros ScoreViewModel no Logcat filtrando por ScoreViewModel. O valor da pontuação precisa ser mostrado.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

Nesta tarefa, você implementou ScoreFragment para usar ViewModel. Você também aprendeu a criar um construtor parametrizado para um ViewModel usando a interface ViewModelFactory.

Parabéns! Você mudou a arquitetura do app para usar um dos Componentes da arquitetura do Android, ViewModel. Você resolveu o problema do ciclo de vida do app, e agora os dados do jogo sobrevivem às mudanças de configuração. Você também aprendeu a criar um construtor parametrizado para criar um ViewModel usando a interface ViewModelFactory.

Projeto do Android Studio: GuessTheWord (link em inglês)

  • As diretrizes de arquitetura de apps do Android recomendam separar classes que têm responsabilidades diferentes.
  • Um controlador de UI é uma classe baseada em UI, como Activity ou Fragment. Os controladores de UI precisam conter somente a lógica que processa as interações entre a UI e o sistema operacional. Eles não podem conter dados a serem exibidos na UI. Coloque esses dados em um ViewModel.
  • A classe ViewModel armazena e gerencia dados relacionados à interface. A classe ViewModel permite que os dados sobrevivam às mudanças de configuração, como a rotação da tela.
  • O ViewModel é um dos Componentes da arquitetura do Android recomendados.
  • ViewModelProvider.Factory é uma interface que pode ser usada para criar um objeto ViewModel.

A tabela abaixo compara os controladores de interface com as instâncias ViewModel que contêm dados para eles:

Controlador de interface

ViewModel

Um exemplo de controlador de UI é o ScoreFragment que você criou neste codelab.

Um exemplo de ViewModel é o ScoreViewModel que você criou neste codelab.

Não contém dados para serem mostrados na interface.

Contém dados que o controlador de UI mostra na interface.

Contém código para mostrar dados e código de eventos do usuário, como listeners de cliques.

Contém código para processamento de dados.

Destruído e recriado durante cada mudança de configuração.

Destruído apenas quando o controlador de interface associado desaparece permanentemente. No caso de uma atividade, quando ela termina, e no caso de um fragmento, quando ele é desanexado.

Contém visualizações.

Nunca deve conter referências a atividades, fragmentos ou visualizações, porque eles não sobrevivem a mudanças de configuração, mas o ViewModel sim.

Contém uma referência ao ViewModel associado.

Não contém nenhuma referência ao controlador de interface associado.

Curso da Udacity:

Documentação do desenvolvedor Android:

Outro:

Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:

  • Atribuir o dever de casa, se necessário.
  • Informar aos alunos como enviar deveres de casa.
  • Atribuir nota aos deveres de casa.

Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.

Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.

Responda estas perguntas

Pergunta 1

Para evitar a perda de dados durante uma mudança na configuração do dispositivo, é preciso salvar dados do app em qual classe?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

Pergunta 2

Um ViewModel não pode conter referências a fragmentos, atividades ou visualizações. Verdadeiro ou falso?

  • Verdadeiro
  • Falso

Pergunta 3

Quando um ViewModel é destruído?

  • Quando o controlador de IU associado é destruído e recriado durante uma mudança na orientação do dispositivo.
  • Em uma mudança de orientação.
  • Quando o controlador de IU associado é concluído (se for uma atividade) ou removido (se for um fragmento).
  • Quando o usuário pressiona o botão "Voltar".

Pergunta 4

Para que serve a interface ViewModelFactory?

  • Instanciar um objeto ViewModel.
  • Reter dados durante mudanças de orientação.
  • Atualizar os dados mostrados na tela.
  • Receber notificações quando os dados do app são modificados.

Comece a próxima lição: 5.2: LiveData e observadores do LiveData

Para acessar links de outros codelabs neste curso, consulte a página inicial dos codelabs de conceitos básicos do Kotlin para Android.