Tela principal

Tela do jogo

Tela de pontuação

Introdução

Neste tutorial, você aprende sobre um dos componentes de arquitetura do Android, ViewModel:

O que você já deveria saber

O que aprenderá

O que fará

Nos tutoriais da Lição 5, você desenvolve o aplicativo GuessTheWord, começando com o código inicial. GuessTheWord é um jogo de estilo charadas para dois jogadores, onde os jogadores colaboram para atingir a pontuação mais alta possível.

O primeiro jogador olha para as palavras no aplicativo e representa cada uma delas, tomando cuidado para não mostrar a palavra ao segundo jogador. O segundo jogador tenta adivinhar a palavra.

Para jogar, o primeiro jogador abre o aplicativo no dispositivo e vê uma palavra, por exemplo "guitarra", conforme mostrado na imagem abaixo.

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

Nesta tarefa, você baixa e executa o aplicativo inicial e examina o código.

Etapa 1: Começar

  1. Baixe o código inicial GuessTheWord e abra o projeto no Android Studio.
  2. Execute o aplicativo em um dispositivo com Android ou em um emulador.
  3. Toque nos botões. Observe que o botão Skip exibe a próxima palavra e diminui a pontuação em um, e o botão Got It mostra a próxima palavra e aumenta a pontuação em um. O botão End Game não está implementado, então nada acontece quando você toca nele.

Etapa 2: Faça um passo a passo do código

  1. No Android Studio, explore o código para ter uma ideia de como o aplicativo funciona.
  2. Certifique-se de olhar os arquivos descritos abaixo, que são particularmente importantes.

MainActivity.kt

Este arquivo contém apenas código gerado por modelo padrão.

res/layout/main_activity.xml

Este arquivo contém o layout principal do aplicativo. O NavHostFragment hospeda os outros fragmentos conforme o usuário navega pelo aplicativo.

Fragmentos de IU

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

screens/title/TitleFragment.kt

O fragmento do título é a primeira tela exibida quando o aplicativo é iniciado. Um tratador de cliques é definido para o botão Play para navegar até a tela do jogo.

screens/game/GameFragment.kt

Este é o fragmento principal, onde ocorre a maior parte da ação do jogo:

screens/score/ScoreFragment.kt

ScoreFragment é a tela final do jogo e exibe a pontuação final do jogador. Neste tutorial, você adiciona a implementação para exibir esta tela e mostrar a pontuação final.

res/navigation/main_navigation.xml

O grafo de navegação mostra como os fragmentos são conectados por meio da navegação:

Nesta tarefa, você encontrará problemas com o aplicativo inicial GuessTheWord.

  1. Execute o código inicial e jogue o jogo com algumas palavras, tocando em Skip ou Got It após cada palavra.
  2. A tela do jogo agora mostra uma palavra e a pontuação atual. Altere a orientação da tela girando o dispositivo ou emulador. Observe que a pontuação atual foi perdida.
  3. Execute o jogo com mais algumas palavras. Quando a tela do jogo for exibida com alguma pontuação, feche e reabra o aplicativo. Observe que o jogo é reiniciado desde o início, pois o estado do aplicativo não é salvo.
  4. Jogue com algumas palavras e, em seguida, toque no botão End Game. Observe que nada acontece.

Problemas no aplicativo:

Você pode resolver esses problemas usando os componentes da arquitetura do aplicativo que aprenderá neste tutorial.

Arquitetura do aplicativo

A arquitetura do aplicativo é uma maneira de projetar as classes de seus aplicativos e os relacionamentos entre eles, de forma que o código seja organizado, tenha um bom desempenho em cenários específicos e seja fácil de trabalhar. Neste conjunto de quatro codelabs, as melhorias que você faz no aplicativo GuessTheWord seguem as diretrizes de arquitetura de aplicativo Android e você usa Componentes de arquitetura Android. A arquitetura do aplicativo Android é semelhante ao padrão de arquiteturaMVVM (em inglês) (model-view-viewmodel).

O aplicativo GuessTheWord segue o princípio de projeto de separação de interesses e é dividido em classes, com cada classe abordando um assunto separado. Neste primeiro tutorial da lição, as classes com as quais você trabalha são um controlador de IU, um ViewModel e um ViewModelFactory.

Controlador de IU

Um controlador de IU é uma classe baseada em IU, como Activity ou Fragment. Um controlador de IU deve conter apenas a lógica que trata com as interações da IU e do sistema operacional, como exibir vistas e capturar a entrada do usuário. Não coloque lógica de tomada de decisão, como a lógica que determina o texto a ser exibido, no controlador de IU.

No código inicial GuessTheWord, os controladores de IU são os três fragmentos: GameFragment, ScoreFragment, e TitleFragment. Seguindo o princípio de design de "separação de interesses", o GameFragment é responsável apenas por desenhar os 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, esta informação é passada para o GameViewModel.

ViewModel

Um ViewModel mantém os dados a serem exibidos em um fragmento ou 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 de IU. Nesta arquitetura, o ViewModel executa a tomada de decisão.

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

ViewModelFactory

Um ViewModelFactory instancia objetos ViewModel, com ou sem parâmetros do construtor.

Em tutoriais posteriores, você aprenderá sobre outros componentes de arquitetura do Android que estão relacionados a controladores de IU e ViewModel.

A classe ViewModel é projetada para armazenar e gerenciar os dados relacionados à IU. Neste aplicativo, cada ViewModel está associado a um fragmento.

Nesta tarefa, você adiciona seu primeiro ViewModel ao seu aplicativo, o GameViewModel para o GameFragment. Você também aprenderá o que significa que o ViewModel reconhece o ciclo de vida.

Etapa 1: Adicione a classe GameViewModel

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

    Se você usar a versão mais recente da biblioteca, o aplicativo de solução deve compilar conforme o esperado. Caso contrário, tente resolver o problema ou reverta para a versão mostrada abaixo.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.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 ajudá-lo 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: Substitua onCleared() e adicionar registro

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

  1. Na classe GameViewModel, substitua o método onCleared().
  2. Adicione uma instrução de registro dentro de onCleared() para rastrear o ciclo de vida de GameViewModel.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

Etapa 3: Associe GameViewModel ao fragmento do jogo

Um ViewModel precisa ser associado a um controlador de IU. Para associar os dois, você cria uma referência ao ViewModel dentro do controlador de IU.

Nesta etapa, você cria uma referência do GameViewModel dentro do controlador de IU correspondente, que é 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: Inicialize o ViewModel

Durante as alterações de configuração, como rotações de tela, os controladores de IU, 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 toda vez que o fragmento for recriado. Em vez disso, crie a instância ViewModel usando um ViewModelProvider.

Como funciona o ViewModelProvider:

Inicialize o ViewModel, usando o método ViewModelProvider.get() para criar um ViewModelProvider:

  1. Na classe GameFragment, inicialize a variável viewModel. Coloque este código dentro de onCreateView(), após a definição da variável de ligação. Use o método ViewModelProvider.get() e passe o contexto GameFragment associado e a classe GameViewModel.
  2. Acima da inicialização do objeto ViewModel, adicione uma instrução de log para registrar a chamada do método ViewModelProvider.get().
Log.i("GameFragment", "Called ViewModelProvider.get")
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
  1. Execute o aplicativo. No Android Studio, abra o painel Logcat e filtre Jogo. Toque no botão Play em seu dispositivo ou emulador. A tela do jogo é aberta.

    Conforme mostrado no Logcat, o método onCreateView() do GameFragment chama o ViewModelProvider.get() método para criar o GameViewModel. As instruções de registro que você adicionou ao GameFragment e ao GameViewModel aparecem no Logcat.
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
  1. Habilite a configuração de rotação automática em seu dispositivo ou emulador e mude a orientação da tela algumas vezes. O GameFragment é destruído e recriado a cada vez, então ViewModelProvider.get() é chamado a cada vez. Mas o GameViewModel é criado apenas uma vez e não é recriado ou destruído para cada chamada.
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
  1. Saia do jogo ou navegue para fora do fragmento do jogo. O GameFragment é destruído. O GameViewModel associado também é destruído e o retorno de chamada onCleared() é chamado.
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel destroyed!

O ViewModel sobrevive às mudanças de configuração, então é um bom lugar para dados que precisam sobreviver às mudanças de configuração:

Para comparação, veja como os dados da IU do GameFragment são manipulados no aplicativo inicial antes de adicionar ViewModel e depois de adicionar ViewModel:

Nesta tarefa, você move os dados da IU do aplicativo para a classe GameViewModel, junto com os métodos para processar os dados. Você faz isso para que os dados sejam retidos durante as mudanças de configuração.

Etapa 1: Mova campos de dados e processamento de dados para ViewModel

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

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

    Não mova a variável de vinculação, GameFragmentBinding, pois contém referências às vistas. Essa variável é usada para aumentar o layout, configurar os ouvintes de clique e exibir os dados na tela - responsabilidades do fragmento.
  2. Mova os métodos resetList() e nextWord(). Esses métodos decidem que palavra mostrar na tela.
  3. De dentro do método onCreateView(), mova as chamadas de método para resetList() e nextWord() para init bloco do GameViewModel.

    Esses métodos devem estar no bloco init, pois você deve redefinir a lista de palavras quando o ViewModel é criado, não sempre que o fragmento é criado. Você pode deletar a declaração de registro no bloco init de GameFragment.

Os tratadores de clique onSkip() e onCorrect() no GameFragment contêm código para processar os dados e atualizar a IU. O código para atualizar a IU deve permanecer no fragmento, mas o código para processar os dados precisa ser movido para o ViewModel.

Por enquanto, coloque os métodos idênticos em ambos os lugares:

  1. Copie os métodos onSkip() e onCorrect() do GameFragment para o GameViewModel.
  2. No GameViewModel, certifique-se de que os métodos onSkip() e onCorrect() não são private, pois você referenciará esses métodos do fragmento.

Aqui está o código para a classe GameViewModel, após a refatoração:

class GameViewModel : ViewModel() {
   var word = ""
   var score = 0
   private lateinit var wordList: MutableList<String>

   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!")
   }
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
   fun onSkip() {
       score--
       nextWord()
   }

   fun onCorrect() {
       score++
       nextWord()
   }

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

Aqui está o código para a classe GameFragment, após a refatoração:

class GameFragment : Fragment() {

   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProvider.get")
       viewModel = ViewModelProvider(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }

   private fun onSkip() {
       score--
       nextWord()
   }

   private fun onCorrect() {
       score++
       nextWord()
   }

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

Etapa 2: Atualize as referências para tratadores de cliques e campos de dados em GameFragment

  1. Em GameFragment, atualize os métodos onSkip() e onCorrect(). Remova o código para atualizar a pontuação e, em vez disso, 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 exibem 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, pois essas variáveis ​​agora estão em o 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 o updateWordText() e updateScoreText() métodos. Esses métodos agora estão sendo chamados do GameFragment.
  2. Crie o aplicativo e certifique-se de que não haja erros. Se você tiver erros, limpe e reconstrua o projeto.
  3. Execute o aplicativo e jogue o jogo com algumas palavras. Enquanto você estiver na tela do jogo, gire o dispositivo. Observe que a pontuação atual e a palavra atual são retidas após a mudança de orientação.

Bom trabalho! Agora todos os dados do seu aplicativo são armazenados em um ViewModel, portanto, são retidos durante as alterações de configuração.

Nesta tarefa, você implementa o ouvinte 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 End Game.
private fun onEndGame() {
   }
  1. Em GameFragment, dentro do método onCreateView(), localize o código que define os ouvintes de clique para Got It e Skip botões. Logo abaixo dessas duas linhas, defina um ouvinte de clique para o botão End Game. Use a variável de vinculação, binding. Dentro do ouvinte de clique, chame o método onEndGame().
binding.endGameButton.setOnClickListener { onEndGame() }
  1. Em GameFragment, adicione um método chamado gameFinished() para navegar pelo aplicativo até a tela de pontuação. Passe a pontuação como um argumento, usando Safe Args.
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 aplicativo, jogue o jogo e percorra algumas palavras. Toque no botão End Game. Observe que o aplicativo navega para a tela de pontuação, mas a pontuação final não é exibida. Você corrige isso na próxima tarefa.

Quando o usuário termina o jogo, o ScoreFragment não mostra a pontuação. Você deseja que um ViewModel mantenha a pontuação a ser exibida pelo ScoreFragment. Você passará o valor da pontuação durante a inicialização do ViewModel usando o padrão do método fábrica.

O padrão de método de fábrica é um padrão de projeto criacional (em inglês) 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ê cria um ViewModel com um construtor parametrizado para o fragmento de pontuação e um método fábrica para instanciar o ViewModel.

  1. No pacote score, crie uma classe Kotlin chamada ScoreViewModel. Esta classe será o ViewModel para o fragmento de pontuação.
  2. Estenda a classe ScoreViewModel de ViewModel. Adicione um parâmetro de construtor para a pontuação final. Adicione um bloco init com uma instrução de log.
  3. Na classe ScoreViewModel, adicione uma variável chamada score para salvar a pontuação final.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. No pacote score, crie outra classe Kotlin chamada ScoreViewModelFactory. Esta classe será responsável por instanciar o objeto ScoreViewModel.
  2. Estenda a classe ScoreViewModelFactory de ViewModelProvider.Factory. Adicione um parâmetro de 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-construído.
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(), após inicializar a variável binding, inicialize a viewModelFactory. Use a ScoreViewModelFactory. Passe 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 (), após inicializar viewModelFactory, inicialize o objeto viewModel. Chame o método ViewModelProvider.get(), passe o contexto do fragmento de pontuação associado e viewModelFactory. Isso criará o objeto ScoreViewModel usando o método de fábrica definido na viewModelFactory classe.
viewModel = ViewModelProvider(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. No método onCreateView(), após inicializar o viewModel, defina o texto da vista scoreText para a pontuação final definido no ScoreViewModel.
binding.scoreText.text = viewModel.score.toString()
  1. Execute seu aplicativo e jogue o jogo. Percorra algumas ou todas as palavras e toque em End Game. Observe que o fragmento de pontuação agora exibe a pontuação final.

  1. Opcional: Verifique os registros de ScoreViewModel no Logcat filtrando em ScoreViewModel. O valor da pontuação deve ser exibido.
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 como criar um construtor parametrizado para um ViewModel usando a interface ViewModelFactory.

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

Projeto Android Studio: GuessTheWord

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

Controlador de IU

ViewModel

Um exemplo de controlador de IU é o ScoreFragment que você criou neste tutorial.

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

Não contém nenhum dado a ser exibido na IU.

Contém dados que o controlador de IU exibe na IU.

Contém código para exibir dados e código de evento do usuário, como ouvintes de clique.

Contém código para processamento de dados.

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

Destruído apenas quando o controlador de IU associado sai permanentemente - para uma atividade, quando a atividade termina, ou para um fragmento, quando o fragmento é desanexado.

Contém vistas.

Nunca deve conter referências a atividades, fragmentos ou vistas, pois eles não sobrevivem às 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 IU associado.

Documentação para desenvolvimento em Android:

De outros:

Esta seção lista as possíveis tarefas de casa para os alunos que estão trabalhando neste tutorial como parte de um curso ministrado por um instrutor.

Responda a essas perguntas

Pergunta 1

Para evitar a perda de dados durante uma alteração na configuração do dispositivo, você deve salvar os dados do aplicativo em qual classe?

Pergunta 2

Um ViewModel nunca deve conter nenhuma referência a fragmentos, atividades ou vistas. Verdadeiro ou falso?

Pergunta 3

Quando um ViewModel é destruído?

Pergunta 4

Para que serve a interface ViewModelFactory?

Comece a próxima lição: 05.2: Observadores LiveData e LiveData

Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.