Introdução

No tutorial anterior, você usou um ViewModel no aplicativo GuessTheWord para permitir que os dados do aplicativo sobrevivam às alterações de configuração do dispositivo. Neste tutorial, você aprende como integrar LiveData com os dados nas classes ViewModel. LiveData, que é um dos Componentes de arquitetura do Android, permite construir objetos de dados que notificam as vistas quando o banco de dados subjacente muda.

Para usar a classe LiveData, você configura "observadores" (por exemplo, atividades ou fragmentos) que observam mudanças nos dados do aplicativo. LiveData reconhece o ciclo de vida, portanto, atualiza apenas observadores de componentes de aplicativos que estão em um estado de ciclo de vida ativo.

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.

Neste tutorial, você aprimora o aplicativo GuessTheWord adicionando um evento para encerrar o jogo quando o usuário percorre todas as palavras no aplicativo. Você também adiciona um botão Play Again no fragmento de pontuação, para que o usuário possa jogar o jogo novamente.

Tela principal

Tela do jogo

Tela de pontuação

Nesta tarefa, você localiza e executa seu código inicial para este tutorial. Você pode usar o aplicativo GuessTheWord que você construiu no tutorial anterior como seu código inicial ou pode baixar um aplicativo inicial.

  1. (Opcional) Se você não estiver usando o código do tutorial anterior, baixe o código inicial para este tutorial. Descompacte o código e abra o projeto no Android Studio.
  2. Execute o aplicativo e jogue o jogo.
  3. 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 encerra o jogo.

LiveData é uma classe de recipiente de dados observável que reconhece o ciclo de vida. Por exemplo, você pode envolver um LiveData em torno da pontuação atual no aplicativo GuessTheWord. Neste tutorial, você aprende sobre várias características do LiveData:

Nesta tarefa, você aprenderá como envolver qualquer tipo de dados em objetos LiveData, convertendo a pontuação atual e os dados da palavra atual no GameViewModel em LiveData. Em uma tarefa posterior, você adiciona um observador a esses objetos LiveData e aprende como observar o LiveData.

Etapa 1: Altere a pontuação e a palavra para usar o LiveData

  1. No pacote screens/game, abra o arquivo GameViewModel.
  2. Altere o tipo das variáveis ​​score e word para MutableLiveData.

    MutableLiveData é um LiveData cujo valor pode ser alterado. MutableLiveData é uma classe genérica, então você precisa especificar o tipo de dados que ela contém.
val word = MutableLiveData<String>()
val score = MutableLiveData<Int>()
  1. Em GameViewModel, dentro do bloco init, inicialize score e word. Para mudar o valor de uma variável LiveData, você usa o método setValue() na variável. No Kotlin, você pode chamar setValue() usando a propriedade value.
init {

   word.value = ""
   score.value = 0
  ...
}

Etapa 2: Atualize a referência do objeto LiveData

As variáveis ​​score e word agora são do tipo LiveData. Nesta etapa, você altera as referências a essas variáveis, usando a propriedade value.

  1. Em GameViewModel, no método onSkip(), altere score para score.value. Observe o erro sobre score possivelmente ser null. Você corrige este erro a seguir.
  2. Para resolver o erro, adicione uma verificação null a score.value em onSkip(). Em seguida, chame a função minus() em score, que realiza a subtração com null-safety.
fun onSkip() {
   score.value = (score.value)?.minus(1)
   nextWord()
}
  1. Atualize o método onCorrect() da mesma maneira: Adicione um cheque null à variável score e use o plus() função.
fun onCorrect() {
   score.value = (score.value)?.plus(1)
   nextWord()
}
  1. Em GameViewModel, dentro do método nextWord(), altere a referência de word para word.value.
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  1. Em GameFragment, dentro do método updateWordText(), altere a referência para viewModel.word para viewModel.word.value.
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}
  1. Em GameFragment, dentro do método updateScoreText(), altere a referência para o viewModel.score para viewModel.score.value.
private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.value.toString()
}
  1. Em GameFragment, dentro do método gameFinished(), altere a referência para viewModel.score para viewModel.score.value. Adicione a verificação de segurança null necessária.
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   NavHostFragment.findNavController(this).navigate(action)
}
  1. Certifique-se de que não haja erros em seu código. Compile e execute seu aplicativo. A funcionalidade do aplicativo deve ser a mesma de antes.

Esta tarefa está intimamente relacionada à tarefa anterior, onde você converteu a pontuação e os dados de palavras em objetos LiveData. Nesta tarefa, você anexa objetos Observer a esses objetos LiveData. Você usará a vista de fragmento (viewLifecycleOwner) como o LifecycleOwner.

  1. Em GameFragment, dentro do método onCreateView(), anexe um objeto Observer ao objeto LiveData para a pontuação atual, viewModel.score. Use o método observe() e coloque o código após a inicialização do viewModel. Use uma expressão lambda para simplificar o código. (Uma expressão lambda é uma função anônima que não é declarada, mas é passada imediatamente como uma expressão).
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
})

Resolva a referência ao Observer. Para fazer isso, clique em Observer, pressione Alt+Enter (Option+Enter em um Mac) e importe androidx.lifecycle.Observer.

  1. O observador que você acabou de criar recebe um evento quando os dados mantidos pelo objeto LiveData observado são alterados. Dentro do observador, atualize a partitura TextView com a nova partitura.
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  1. Anexe um objeto Observer ao objeto LiveData da palavra atual. Faça da mesma forma que anexou um objeto Observer à partitura atual.
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
   binding.wordText.text = newWord
})

Quando o valor de score ou a word muda, a score ou word exibida na tela agora é atualizada automaticamente.

  1. Em GameFragment, exclua os métodos updateWordText() e updateScoreText(), e todas as referências a eles. Você não precisa mais deles, pois as vistas de texto são atualizadas pelos métodos do observador LiveData.
  2. Execute seu aplicativo. Seu aplicativo de jogo deve funcionar exatamente como antes, mas agora ele usa observadores LiveData e LiveData.

Encapsulamento é uma forma de restringir o acesso direto a alguns dos campos de um objeto. Ao encapsular um objeto, você expõe um conjunto de métodos públicos que modificam os campos internos privados. Usando o encapsulamento, você controla como outras classes manipulam esses campos internos.

Em seu código atual, qualquer classe externa pode modificar as variáveis ​​score e word usando a propriedade value, por exemplo, usando viewModel.score.value. Pode não importar no aplicativo que você está desenvolvendo neste tutorial, mas em um aplicativo de produção, você deseja controlar os dados nos objetos ViewModel.

Apenas o ViewModel deve editar os dados em seu aplicativo. Mas os controladores de IU precisam ler os dados, então os campos de dados não podem ser privados. Para encapsular os dados do seu aplicativo, você usa os objetos MutableLiveData e LiveData.

MutableLiveData vs. LiveData:

Para executar essa estratégia, você usa uma propriedade de apoio do Kotlin. Uma propriedade backing permite que você retorne algo de um getter diferente do objeto exato. Nesta tarefa, você implementa uma propriedade de apoio para os objetos score e word no aplicativo GuessTheWord.

Adicione uma propriedade de apoio à pontuação e à palavra

  1. Em GameViewModel, torne o objeto score atual private.
  2. Para seguir a convenção de nomenclatura usada nas propriedades de apoio, altere score para _score. A propriedade _score agora é a versão mutável da pontuação do jogo, para ser usada internamente.
  3. Crie uma versão pública do tipo LiveData, chamada score.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  1. Você vê um erro de inicialização. Este erro ocorre, pois dentro do GameFragment, o score é uma referência do LiveData e score não pode mais acessar seu setter. Para saber mais sobre getters e setters em Kotlin, consulte Getters e Setters.

    Para resolver o erro, substitua get() método para o objeto score em GameViewModel e retorna a propriedade de apoio, _score.
val score: LiveData<Int>
   get() = _score
  1. No GameViewModel, altere as referências de score para sua versão mutável interna, _score.
init {
   ...
   _score.value = 0
   ...
}

...
fun onSkip() {
   _score.value = (score.value)?.minus(1)
  ...
}

fun onCorrect() {
   _score.value = (score.value)?.plus(1)
   ...
}
  1. Renomeie o objeto word para _word e adicione uma propriedade de apoio para ele, como você fez para o objeto score.
private val _word = MutableLiveData<String>()
val word: LiveData<String>
   get() = _word
...
init {
   _word.value = ""
   ...
}
...
private fun nextWord() {
   if (!wordList.isEmpty()) {
       _word.value = wordList.removeAt(0)
   }
}

Ótimo trabalho, você encapsulou os objetos LiveData word e score.

Seu aplicativo atual navega para a tela de pontuação quando o usuário toca no botão End Game. Você também deseja que o aplicativo navegue até a tela de pontuação quando os jogadores tiverem passado por todas as palavras. Depois que os jogadores terminarem com a última palavra, você deseja que o jogo termine automaticamente para que o usuário não precise tocar no botão.

Para implementar esta funcionalidade, você precisa que um evento seja disparado e comunicado ao fragmento do ViewModel quando todas as palavras forem mostradas. Para fazer isso, você usa o padrão de observador LiveData para modelar um evento de jogo terminado.

O padrão do observador

O padrão do observador é um padrão de projeto de software. Ele especifica a comunicação entre objetos: Um observável (o "sujeito" da observação) e observadores. Um observável é um objeto que notifica os observadores sobre as mudanças em seu estado.

No caso de LiveData neste aplicativo, o observável (sujeito) é o objeto LiveData e os observadores são os métodos nos controladores de IU, como fragmentos. Uma mudança de estado ocorre sempre que os dados agrupados em LiveData são alterados. As classes LiveData são cruciais na comunicação do ViewModel para o fragmento.

Etapa 1: Use o LiveData para detectar um evento de finalização do jogo

Nesta tarefa, você usa o padrão de observador LiveData para modelar um evento de jogo terminado.

  1. Em GameViewModel, crie um objeto Boolean MutableLiveData chamado _eventGameFinish. Este objeto conterá o evento de finalização do jogo.
  2. Após inicializar o objeto _eventGameFinish, crie e inicialize uma propriedade de apoio chamada eventGameFinish.
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish
  1. Em GameViewModel, adicione um método onGameFinish(). No método, defina o evento de finalização do jogo, eventGameFinish, como true.
fun onGameFinish() {
   _eventGameFinish.value = true
}
  1. Em GameViewModel, dentro do método nextWord(), encerre o jogo se a lista de palavras estiver vazia.
private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}
  1. Em GameFragment, dentro de onCreateView(), após inicializar o viewModel, anexe um observador a eventGameFinish. Use o método observe(). Dentro da função lambda, chame o método gameFinished().
viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  1. Execute seu aplicativo, jogue o jogo e repasse todas as palavras. O aplicativo navega para a tela de pontuação automaticamente, em vez de permanecer no fragmento do jogo até que você toque em End Game.

    Depois que a lista de palavras está vazia, eventGameFinish é definido, o método de observador associado no fragmento do jogo é chamado e o aplicativo navega para o fragmento de tela.
  2. O código adicionado introduziu um problema de ciclo de vida. Para entender o problema, na classe GameFragment, comente o código de navegação no método gameFinished(). Certifique-se de manter a mensagem Toast no método.
private fun gameFinished() {
       Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
//        val action = GameFragmentDirections.actionGameToScore()
//        action.score = viewModel.score.value?:0
//        NavHostFragment.findNavController(this).navigate(action)
   }
  1. Execute seu aplicativo, jogue o jogo e repasse todas as palavras. Uma mensagem toast que diz "Game has just finished" aparece brevemente na parte inferior da tela do jogo, que é o comportamento esperado.

Agora gire o dispositivo ou emulador. O toast é exibido novamente! Gire o dispositivo mais algumas vezes e provavelmente você verá a toast todas as vezes. Isso é um erro, pois a toast somente deve ser exibida uma vez, quando o jogo terminar. A toast não deve ser exibida sempre que o fragmento for recriado. Você resolverá esse problema na próxima tarefa.

Etapa 2: Redefina o evento de finalização do jogo

Normalmente, o LiveData entrega atualizações aos observadores apenas quando os dados são alterados. Uma exceção a esse comportamento é que os observadores também recebem atualizações quando o observador muda de um estado inativo para um estado ativo.

É por isso que o toast ao final do jogo é acionado repetidamente em seu aplicativo. Quando o fragmento do jogo é recriado após uma rotação da tela, ele passa de um estado inativo para um estado ativo. O observador no fragmento é reconectado ao ViewModel existente e recebe os dados atuais. O método gameFinished() é disparado novamente e a notificação do sistema é exibida.

Nesta tarefa, você corrige este problema e exibe a notificação do sistema apenas uma vez, redefinindo o sinalizador eventGameFinish no GameViewModel.

  1. Em GameViewModel, adicione um método onGameFinishComplete() para redefinir o evento de finalização do jogo, _eventGameFinish.
fun onGameFinishComplete() {
   _eventGameFinish.value = false
}
  1. Em GameFragment, no final de gameFinished(), chame onGameFinishComplete() no viewModel objeto. (Deixe o código de navegação em gameFinished() comentado por enquanto).
private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}
  1. Execute o aplicativo e jogue o jogo. Repasse todas as palavras e altere a orientação da tela do dispositivo. A toast é exibida apenas uma vez.
  2. Em GameFragment, dentro do método gameFinished(), descomente o código de navegação.

    Para descomentar no Android Studio, selecione as linhas que estão comentadas e pressione Control+/ (Command+/ em um Mac).
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   findNavController(this).navigate(action)
   viewModel.onGameFinishComplete()
}

Se solicitado pelo Android Studio, importe androidx.navigation.fragment.NavHostFragment.findNavController.

  1. Execute o aplicativo e jogue o jogo. Certifique-se de que o aplicativo navegue automaticamente para a tela de pontuação final depois de passar por todas as palavras.

Bom trabalho! Seu aplicativo usa LiveData para acionar um evento de jogo terminado para se comunicar do GameViewModel para o fragmento do jogo que a lista de palavras está vazia. O fragmento do jogo então navega para o fragmento de pontuação.

Nesta tarefa, você altera a pontuação para um objeto LiveData no ScoreViewModel e anexa um observador a ele. Esta tarefa é semelhante ao que você fez quando adicionou LiveData ao GameViewModel.

Você faz essas alterações em ScoreViewModel para integridade, de modo que todos os dados em seu aplicativo usem LiveData.

  1. Em ScoreViewModel, altere o tipo de variável score para MutableLiveData. Renomeie-o por convenção para _score e adicione uma propriedade de apoio.
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score
  1. Em ScoreViewModel, dentro do bloco init, inicialize _score. Você pode remover ou deixar o registro no bloco init como desejar.
init {
   _score.value = finalScore
}
  1. Em ScoreFragment, dentro de onCreateView(), após inicializar o viewModel, anexe um observador para a pontuação LiveData. Dentro da expressão lambda, defina o valor da pontuação para a vista do texto da pontuação. Remova o código que atribui diretamente a vista de texto com o valor de pontuação do ViewModel.

Código para adicionar:

viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

Código para remover:

binding.scoreText.text = viewModel.score.toString()

Quando solicitado pelo Android Studio, importe androidx.lifecycle.Observer.

  1. Execute seu aplicativo e jogue o jogo. O aplicativo deve funcionar como antes, mas agora usa LiveData e um observador para atualizar a partitura.

Nesta tarefa, você adiciona um botão Play Again à tela de pontuação e implementa seu ouvinte de clique usando um evento LiveData. O botão aciona um evento para navegar da tela de pontuação para a tela do jogo.

O código inicial do aplicativo inclui o botão Play Again, mas o botão está oculto.

  1. Em res/layout/score_fragment.xml, para o botão play_again_button, altere a visibility valor do atributo para visible.
<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />
  1. Em ScoreViewModel, adicione um objeto LiveData para conter um Boolean chamado _eventPlayAgain. Este objeto é usado para salvar o evento LiveData para navegar da tela de pontuação para a tela do jogo.
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain
  1. Em ScoreViewModel, defina métodos para definir e redefinir o evento, _eventPlayAgain.
fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}
  1. Em ScoreFragment, adicione um observador para eventPlayAgain. Coloque o código no final de onCreateView(), antes da instrução return. Dentro da expressão lambda, navegue de volta para a tela do jogo e redefina eventPlayAgain.
viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})

Importe androidx.navigation.fragment.findNavController, quando solicitado pelo Android Studio.

  1. Em ScoreFragment, dentro de onCreateView(), adicione um ouvinte de clique ao botão PlayAgain e chame viewModel.onPlayAgain().
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }
  1. Execute seu aplicativo e jogue o jogo. Quando o jogo termina, a tela de pontuação mostra a pontuação final e o botão Play Again. Toque no botão PlayAgain e o aplicativo navegará até a tela do jogo para que você possa jogar novamente.

Bom trabalho! Você alterou a arquitetura de seu aplicativo para usar objetos LiveData no ViewModel e anexou observadores aos objetos LiveData. LiveData notifica objetos observadores quando o valor mantido pelo LiveData muda.

Projeto Android Studio: GuessTheWord

LiveData

Para adicionar LiveData

MutableLiveData é um objeto LiveData cujo valor pode ser alterado. MutableLiveData é uma classe genérica, portanto, você precisa especificar o tipo de dados que ela contém.

Para encapsular LiveData

LiveData observável

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

Como você encapsula o LiveData armazenado em um ViewModel para que objetos externos possam ler os dados sem poder atualizá-los?

Pergunta 2

LiveData atualiza um controlador de IU (como um fragmento) se o controlador de IU estiver em qual dos seguintes estados?

Pergunta 3

No padrão de observador LiveData, qual é o item observável (o que é observado)?

Comece a próxima lição: 05.3: Vinculação de dados com ViewModel e LiveData

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