Tela principal |
Tela do jogo |
Tela de pontuação |
Neste tutorial, você aprende sobre um dos componentes de arquitetura do Android, ViewModel
:
ViewModel
para armazenar e gerenciar dados relacionados à IU de uma maneira
consciente do ciclo de vida. A classe ViewModel
permite que os dados sobrevivam às mudanças de
configuração do dispositivo, como rotações de tela e mudanças na disponibilidade do teclado.ViewModelFactory
para instanciar e retornar o objeto
ViewModel
que sobrevive às mudanças de configuração.Lifecycle
, ViewModel
e ViewModelFactory
classes em seu aplicativo.ViewModel
usando a interface ViewModelProvider.Factory
.
ViewModel
ao aplicativo, para salvar os dados do aplicativo
de forma que os
dados sobrevivam às mudanças de configuração.ViewModelFactory
e o padrão de projeto do método fábrica
para instanciar um
objeto ViewModel
com parâmetros do construtor. 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.
Este arquivo contém apenas código gerado por modelo padrão.
Este arquivo contém o layout principal do aplicativo. O NavHostFragment
hospeda os outros fragmentos conforme o usuário navega pelo
aplicativo.
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 principalgame/GameFragment
para a tela do jogoscore/ScoreFragment
para a tela de pontuaçãoO 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.
Este é o fragmento principal, onde ocorre a maior parte da ação do jogo:
wordList
definida dentro do método resetList()
é um exemplo de lista de palavras
a serem usadas no jogo.onSkip()
é o tratador de cliques para o botão Skip. Ele diminui a
pontuação em 1 e exibe a próxima palavra usando o método nextWord()
.onCorrect()
é o tratador de cliques para o botão Got It. Este método
é implementado de forma semelhante ao método onSkip()
. A única diferença é que este método
adiciona 1 à pontuação em vez de subtrair. 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.
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.
Problemas no aplicativo:
onSaveInstanceState()
retorno de chamada. No entanto, o uso
do método
onSaveInstanceState()
exige que você escreva um código extra para salvar o estado em um pacote e
implemente a lógica para recuperar esse estado. Além disso, a quantidade de dados que pode ser armazenada é
mínima. Você pode resolver esses problemas usando os componentes da arquitetura do aplicativo que aprenderá neste tutorial.
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
.
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
.
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.
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.
build.gradle(module:app)
. Dentro do bloco dependencies
, adicione a
dependência Gradle para o ViewModel
.//ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
screens/game/
, crie uma classe Kotlin chamada GameViewModel
.
GameViewModel
estender a classe abstrata ViewModel
.ViewModel
reconhece o ciclo de vida, adicione um bloco
init
com uma instrução log
. class GameViewModel : ViewModel() {
init {
Log.i("GameViewModel", "GameViewModel created!")
}
}
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.
GameViewModel
, substitua o método onCleared()
.onCleared()
para rastrear o ciclo de vida de
GameViewModel
.override fun onCleared() {
super.onCleared()
Log.i("GameViewModel", "GameViewModel destroyed!")
}
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
.
GameFragment
, adicione um campo do tipo GameViewModel
no nível superior
como uma variável de classe.private lateinit var viewModel: GameViewModel
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
:
ViewModelProvider
retorna um ViewModel
existente, se houver, ou cria um novo, se
ainda não existir.ViewModelProvider
cria uma instância ViewModel
em associação com o escopo
fornecido (uma atividade ou fragmento).ViewModel
criado é retido enquanto o escopo estiver ativo. Por exemplo, se o escopo for um
fragmento, o ViewModel
é retido até que o fragmento seja separado.Inicialize o ViewModel
, usando o método ViewModelProvider.get()
para criar um
ViewModelProvider
:
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
.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)
Jogo
.
Toque no botão Play em seu dispositivo ou emulador. A tela do jogo é aberta.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!
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
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:
ViewModel
.ViewModel
nunca deve conter referências a fragmentos, atividades ou vistas, pois atividades,
fragmentos e vistas não sobrevivem à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
:
ViewModel
:ViewModel
e mover os dados da IU do fragmento do jogo para o
ViewModel
:ViewModel
.
Quando o aplicativo passa por uma alteração de configuração, o ViewModel
sobrevive e os dados são
retidos.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.
Mova os seguintes campos de dados e métodos de GameFragment
para GameViewModel
:
word
, score
e wordList
. Certifique-se de que
word
e score
não são private
.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.resetList()
e nextWord()
. Esses métodos decidem que palavra
mostrar na tela.onCreateView()
, mova as chamadas de método para resetList()
e
nextWord()
para init
bloco do GameViewModel
. 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:
onSkip()
e onCorrect()
do GameFragment
para o
GameViewModel
.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()
}
}
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
.nextWord()
para o ViewModel
, o fragmento do jogo não pode
mais acessá-lo.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()
}
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()
}
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
.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.
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() {
}
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() }
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)
}
onEndGame()
, chame o método gameFinished()
.private fun onEndGame() {
gameFinished()
}
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
.
score
, crie uma classe Kotlin chamada ScoreViewModel
. Esta classe será o
ViewModel
para o fragmento de pontuação. ScoreViewModel
de ViewModel
. Adicione um parâmetro de construtor
para a pontuação final. Adicione um bloco init
com uma instrução de log.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")
}
}
score
, crie outra classe Kotlin chamada ScoreViewModelFactory
. Esta
classe será responsável por instanciar o objeto ScoreViewModel
.ScoreViewModelFactory
de ViewModelProvider.Factory
. Adicione um
parâmetro de construtor para a pontuação final. class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
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")
}
ScoreFragment
, crie variáveis de classe para ScoreViewModel
e
ScoreViewModelFactory
.private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
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)
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)
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()
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
Activity
ou Fragment
. Os controladores de IU devem conter apenas lógica que trata com
as interações da IU e do sistema operacional; eles não devem conter dados a serem exibidos na IU. Coloque
esses dados em um ViewModel
.ViewModel
armazena e gerencia dados relacionados à IU. A
classe
ViewModel
permite que os dados sobrevivam às mudanças de configuração, como rotações de tela.
ViewModel
é um dos Componentes de arquitetura Android recomendados.ViewModelProvider.Factory
é uma interface que você pode usar para criar um objeto
ViewModel
.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 |
Um exemplo de um |
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 |
Contém uma referência ao |
Não contém nenhuma referência ao controlador de IU associado. |
Documentação para desenvolvimento em Android:
ViewModelProvider
ViewModelProvider.Factory
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.
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?
ViewModel
LiveData
Fragment
Activity
Um ViewModel
nunca deve conter nenhuma referência a fragmentos, atividades ou vistas. Verdadeiro
ou falso?
Quando um ViewModel
é destruído?
Para que serve a interface ViewModelFactory
?
ViewModel
.Comece a próxima lição:
Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.