1. Antes de começar
Você aprendeu nos codelabs anteriores sobre o ciclo de vida de atividades e fragmentos, além dos problemas relacionados com as mudanças de configuração. Para salvar os dados do app, uma opção é salvar o estado da instância, mas isso inclui algumas limitações. Neste codelab, você vai aprender uma maneira robusta de projetar seu app e armazenar os dados dele durante mudanças de configuração, usando as bibliotecas do Android Jetpack.
As bibliotecas do Android Jetpack são um conjunto de bibliotecas que facilitam o desenvolvimento de excelentes apps Android. Elas ajudam você a seguir as práticas recomendadas, eliminar o código boilerplate e simplificar tarefas complexas para que seja possível se concentrar na parte importante do código, como a lógica do app.
Os Componentes da arquitetura do Android fazem parte das bibliotecas do Android Jetpack e ajudam você a projetar apps com uma boa arquitetura. Esses Componentes fornecem orientação sobre a arquitetura de apps, e são a prática recomendada.
A arquitetura do app é um conjunto de regras de design. Assim como a planta de uma casa, a arquitetura fornece a estrutura do app. Uma boa arquitetura de app pode tornar seu código robusto, flexível, escalonável e sustentável por anos.
Neste codelab, você aprenderá a usar o ViewModel
, um dos Componentes da arquitetura para armazenar os dados do app. Os dados armazenados não serão perdidos se o framework destruir e recriar as atividades e fragmentos durante uma mudança de configuração ou outros eventos.
Pré-requisitos
- Fazer o download do código-fonte no GitHub e abrir no Android Studio.
- Saber criar e executar um app Android básico em Kotlin, usando atividades e fragmentos.
- Conhecimento sobre o campo de texto do Material Design e widgets comuns da IU, como
TextView
eButton
. - Saber usar a vinculação de visualizações no app.
- Conceitos básicos sobre o ciclo de vida da atividade e do fragmento.
- Saber como adicionar informações de registro a um app e ler registros usando o Logcat no Android Studio.
O que você vai aprender
- Introdução aos princípios básicos da arquitetura de apps Android.
- Como usar a classe
ViewModel
no seu app. - Como armazenar dados da IU após mudanças na configuração do dispositivo usando um
ViewModel
. - Propriedades de apoio em Kotlin.
- Como usar o
MaterialAlertDialog
(link em inglês) da biblioteca de componentes do Material Design.
O que você vai criar
- Um app de jogo Unscramble (link em inglês), em que o usuário consegue adivinhar as palavras.
O que é necessário
- Um computador com o Android Studio instalado.
- O código inicial (link em inglês) do app Unscramble.
2. Visão geral do app inicial
Visão geral do jogo
O app Unscramble é um jogo de palavras embaralhadas para um só jogador. O app exibe uma palavra embaralhada por vez, e o jogador precisa adivinhá-la usando todas as letras disponíveis. O jogador vai marcar pontos se a palavra estiver correta. Se errar, poderá continuar adivinhando quantas vezes quiser. O app também tem a opção de pular a palavra atual. No canto superior esquerdo, o app exibe a contagem de palavras, que é o número de palavras jogadas na sessão atual. Cada partida tem 10 palavras.
Fazer o download do código inicial
Este codelab oferece um código inicial para você se aprofundar nos recursos ensinados. O código inicial pode conter tanto um código familiar quanto desconhecido de codelabs anteriores. Você vai aprender mais sobre códigos desconhecidos nos próximos codelabs.
Se você usar o código inicial do GitHub, o nome da pasta será android-basics-kotlin-unscramble-app-starter
. Selecione essa pasta ao abrir o projeto no Android Studio.
- Navegue até a página do repositório do GitHub fornecida para o projeto.
- Verifique se o nome da ramificação corresponde ao especificado no codelab. Por exemplo, na captura de tela a seguir, o nome da ramificação é main.
- Na página do GitHub do projeto, clique no botão Code, que vai mostrar uma janela pop-up.
- Na janela pop-up, clique no botão Download ZIP para salvar o projeto no seu computador. Aguarde a conclusão do download.
- Localize o arquivo no computador. Geralmente ele é salvo na pasta Downloads.
- Clique duas vezes para descompactar o arquivo ZIP. Isso cria uma nova pasta com os arquivos do projeto.
Abrir o projeto no Android Studio
- Inicie o Android Studio.
- Na janela Welcome to Android Studio, clique em Open.
Observação: caso o Android Studio já esteja aberto, selecione a opção File > Open.
- No navegador de arquivos, vá até a pasta descompactada do projeto, que provavelmente está na pasta Downloads.
- Clique duas vezes nessa pasta do projeto.
- Aguarde o Android Studio abrir o projeto.
- Clique no botão Run
para criar e executar o app. Confira se ele é criado da forma esperada.
Visão geral do código inicial
- Abra o projeto com o código inicial no Android Studio.
- Execute o app em um dispositivo Android ou em um emulador.
- Jogue uma partida com algumas palavras, tocando nos botões Submit e Skip. Tocar nos botões exibe a próxima palavra e aumenta a contagem de palavras.
- A pontuação aumenta apenas ao tocar no botão Submit.
Problemas com o código inicial
Enquanto jogava, é possível que você tenha percebido os seguintes bugs:
- Ao clicar no botão Submit, o app não verifica a palavra do jogador. O jogador sempre marca pontos.
- Não há como encerrar o jogo. O app permite que você jogue mais de 10 palavras.
- A tela do jogo mostra uma palavra embaralhada, a pontuação dos jogadores e a contagem de palavras. Gire o dispositivo ou emulador para mudar a orientação da tela. A palavra, a pontuação e a contagem de palavras atuais são perdidas e o jogo reinicia do início.
Principais problemas no app
O app inicial não salva e restaura o estado e os dados dele durante mudanças na configuração, como quando a orientação do dispositivo muda.
Resolva 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 uma lógica para acessar esse estado. Além disso, a quantidade de dados que pode ser armazenada é mínima.
É possível resolver esses problemas usando os Componentes da arquitetura do Android que você aprenderá neste programa.
Como usar o código inicial
O código inicial que foi transferido por download tem o layout da tela de jogo pré-criado para você. Nesse módulo, você vai implementar a lógica do jogo. Você usará os Componentes da arquitetura para implementar a arquitetura recomendada do app e resolver os problemas mencionados acima. Confira a seguir um breve tutorial sobre alguns dos arquivos para começar.
game_fragment.xml
- Abra
res/layout/game_fragment.xml
na visualização Design. - Esse arquivo contém o layout da única tela do app, que é a tela do jogo.
- Esse layout contém um campo de texto para a palavra do jogador, além de
TextViews
para exibir a pontuação e a contagem de palavras. Há também instruções e botões (Submit e Skip) para jogar.
main_activity.xml
Define o layout da atividade principal com um único fragmento de jogo.
Pasta res/values
Você já conhece os arquivos de recurso nesta pasta.
- O arquivo
colors.xml
contém as cores de tema usadas no app. - O arquivo
strings.xml
contém todas as strings que o app precisa. - As pastas
themes
estyles
contêm a personalização da IU para o app
MainActivity.kt
Contém o código padrão gerado pelo modelo para definir a visualização do conteúdo da atividade como main_activity.xml.
ListOfWords.kt
Esse arquivo contém uma lista das palavras usadas no jogo, além das constantes para o número máximo de palavras por partida e o número de pontos que o jogador marca para cada palavra correta.
GameFragment.kt
Esse é o único fragmento do app. É nele que a maior parte do jogo acontece:
- As variáveis são definidas para a palavra embaralhada atual (
currentScrambledWord
), contagem de palavras (currentWordCount
) e a pontuação (score
). - A instância do objeto de vinculação com o nome
binding
e acesso às visualizaçõesgame_fragment
está definida. - A função
onCreateView()
infla o XML do layoutgame_fragment
usando o objeto de vinculação. - A função
onViewCreated()
configura os listeners de cliques no botão e atualiza a IU. - O método
onSubmitWord()
é o listener de clique do botão Submit. Essa função exibe a próxima palavra embaralhada, limpa o campo de texto e aumenta a pontuação e a contagem da palavra sem validar a palavra do jogador. - O método
onSkipWord()
é o listener de clique do botão Skip. Essa função atualiza a IU de forma semelhante ao métodoonSubmitWord()
, mas não aumenta a pontuação. getNextScrambledWord()
é uma função auxiliar que escolhe uma palavra aleatória da lista de palavras e embaralha as letras dela.- As funções
restartGame()
eexitGame()
são usadas para reiniciar e finalizar o jogo, respectivamente. Você usará essas funções mais tarde. - O método
setErrorTextField()
limpa o conteúdo do campo de texto e redefine o status do erro. - A função
updateNextWordOnScreen()
exibe a nova palavra embaralhada.
3. Saiba mais sobre a arquitetura de apps
A arquitetura fornece as diretrizes para ajudar você a alocar responsabilidades no app entre as classes. Uma arquitetura de app bem projetada ajuda a escalonar e ampliar o app, adicionando outros recursos no futuro. Isso também facilita a colaboração em equipe.
Os princípios de arquitetura mais comuns são a separação de conceitos e o modelo de base da IU.
Separação de conceitos
A separação de conceitos é o princípio que determina que o app precisa ser dividido em classes, cada uma com responsabilidades separadas.
Basear a IU em um modelo
Outro princípio importante é que você precisa basear sua IU em um modelo, de preferência um que seja persistente. Modelos são componentes responsáveis por manipular os dados de um app. Eles são independentes dos objetos Views
e dos componentes do app, portanto, não são afetados pelo ciclo de vida do app nem pelos conceitos associados.
As principais classes ou Componentes da arquitetura do Android são os controladores de IU (atividade/fragmento), o ViewModel
, o LiveData
e o Room
. Esses componentes cuidam de parte da complexidade do ciclo de vida e ajudam a evitar problemas relacionados a ele. Você aprenderá sobre o LiveData
e o Room
nos próximos codelabs.
Este diagrama mostra uma parte básica da arquitetura:
Controladores de IU (atividade / fragmento)
Atividades e fragmentos são controladores de IU. Os controladores de IU comandam as ações da IU mostrando visualizações na tela, capturando eventos e todas as ações relacionadas à IU com que o usuário interage. Os dados no app ou qualquer lógica de tomada de decisão sobre esses dados não podem estar nas classes de controlador de IU.
O sistema Android pode destruir controladores de IU a qualquer momento com base em determinadas interações do usuário ou condições do sistema, como pouca memória. Como esses eventos não estão sob seu controle, não armazene dados ou estados de app em controladores de IU. Em vez disso, a lógica de tomada de decisão sobre os dados precisa ser adicionada ao ViewModel
.
Por exemplo, no app Unscramble, a palavra embaralhada, a contagem de palavras e a pontuação são exibidas em um fragmento (controlador de IU). O código de tomada de decisão, como a descoberta da próxima palavra embaralhada, e os cálculos da pontuação e da contagem de palavras precisam estar no ViewModel
.
ViewModel
O ViewModel
é um modelo dos dados do app exibidos nas visualizações. Modelos são componentes responsáveis por manipular os dados de um app. Eles permitem que seu app siga o princípio da arquitetura, fornecendo a base da IU pelo modelo.
O ViewModel
armazena os dados relacionados ao app que não são destruídos quando a atividade ou o fragmento são destruídos e recriados pelo framework do Android. Os objetos ViewModel
são mantidos automaticamente (não são destruídos como a atividade ou uma instância de fragmento) durante as mudanças de configuração. Assim, os dados mantidos ficam imediatamente disponíveis para a próxima atividade ou instância de fragmento.
Para implementar o ViewModel
no seu app, estenda a classe ViewModel
, que é da biblioteca de Componentes da arquitetura, e armazene os dados do app nessa classe.
Para resumir:
Responsabilidades de fragmento / atividade (controlador de IU) | Responsabilidades |
Atividades e fragmentos são responsáveis por mostrar visualizações e dados na tela e responder aos eventos do usuário. | O |
4. Adicionar um ViewModel
Nesta tarefa, você vai adicionar um ViewModel
ao app para armazenar dados do app (a palavra embaralhada, a contagem de palavras e a pontuação).
Seu app será arquitetado da seguinte maneira. A MainActivity
terá um GameFragment
, e o GameFragment
vai acessar informações sobre o jogo no GameViewModel
.
- Na janela Android do Android Studio na pasta Gradle Scripts, abra o arquivo
build.gradle(Module:Unscramble.app)
. - Para usar o
ViewModel
no seu app, verifique se você tem a dependência da biblioteca do ViewModel no blocodependencies
. Esta etapa já foi concluída. Dependendo da versão mais recente da biblioteca, o número da versão no código gerado pode ser diferente.
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
Recomendamos usar sempre a versão mais recente da biblioteca, em vez da versão mencionada no codelab.
- Crie um novo arquivo de classe do Kotlin com o nome
GameViewModel
. Na janela Android, clique com o botão direito do mouse na pasta ui.game. Selecione New > Kotlin File/Class.
- Nomeie o arquivo como
GameViewModel
e selecione Class na lista. - Mude
GameViewModel
para ser uma subclasse doViewModel
. OViewModel
é uma classe abstrata, então precisa ser estendido para ser usado no seu app. Confira a definição da classeGameViewModel
abaixo.
class GameViewModel : ViewModel() {
}
Anexar o ViewModel ao fragmento
Para associar um ViewModel
a um controlador de IU (atividade / fragmento), crie uma referência (objeto) ao ViewModel
no controlador de IU.
Nesta etapa, você vai criar uma instância de objeto do GameViewModel
no controlador de IU correspondente, que é o GameFragment
.
- Na parte superior da classe
GameFragment
, adicione uma propriedade do tipoGameViewModel
. - Inicialize o
GameViewModel
usando o delegado da propriedadeby viewModels()
do Kotlin. Você vai aprender mais sobre isso na próxima seção.
private val viewModel: GameViewModel by viewModels()
- Se solicitado pelo Android Studio, importe
androidx.fragment.app.viewModels
.
Delegado de propriedade do Kotlin
Em Kotlin, cada propriedade mutável (var
) tem funções getter e setter geradas automaticamente. As funções setter e getter são chamadas quando você atribui um valor ou lê o valor da propriedade.
Para uma propriedade somente leitura (val
), o comportamento é um pouco diferente de uma propriedade mutável. Somente a função getter é gerada por padrão. Essa função getter é chamada quando você lê o valor de uma propriedade somente leitura.
A delegação de propriedades no Kotlin ajuda a passar a responsabilidade do getter-setter para uma classe diferente.
Essa classe, conhecida como classe delegada, fornece funções getter e setter da propriedade e processa as mudanças.
Uma propriedade delegada é definida usando a cláusula by
e uma instância da classe delegada:
// Syntax for property delegation
var <property-name> : <property-type> by <delegate-class>()
No seu app, se você inicializar o modelo de visualização usando o construtor padrão GameViewModel
, como no exemplo abaixo:
private val viewModel = GameViewModel()
O app perderá o estado da referência viewModel
quando uma mudança de configuração ocorrer no dispositivo. Por exemplo, se você girar o dispositivo, a atividade será destruída e criada novamente, e você terá uma nova instância do modelo de visualização com o estado inicial novamente.
Em vez disso, use a abordagem de delegação de propriedade e delegue a responsabilidade do objeto viewModel
a uma classe separada com o nome viewModels
. Isso significa que quando você acessar o objeto viewModel
, ele será processado internamente pela classe delegada, viewModels
. Ela criará o objeto viewModel
para você no primeiro acesso, manterá o valor dele durante as mudanças de configuração e retornará o valor quando solicitado.
5. Transferir dados para o ViewModel
Separar os dados de IU do seu app do controlador de IU (suas classes Activity
/ Fragment
) permite seguir melhor o princípio de responsabilidade exclusiva discutido acima. Suas atividades e fragmentos são responsáveis por mostrar visualizações e dados na tela, enquanto o ViewModel
é responsável por armazenar e processar todos os dados necessários da IU.
Nesta tarefa, você moverá as variáveis de dados do GameFragment
para a classe GameViewModel
.
- Mova as variáveis de dados
score
,currentWordCount
ecurrentScrambledWord
para a classeGameViewModel
.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- Note que há erros sobre referências não resolvidas. Isso ocorre porque as propriedades são particulares para o
ViewModel
e não podem ser acessadas pelo seu controlador de IU. Você corrigirá esses erros em seguida.
Para resolver esse problema, não é recomendado criar modificadores de visibilidade para tornar as propriedades public
. Esses dados não podem ser editados por outras classes. Isso é arriscado porque uma classe externa pode mudar os dados de maneiras inesperadas que não seguem as regras de jogo especificadas no modelo de visualização. Por exemplo, uma classe externa pode mudar a score
para um valor negativo.
No ViewModel
, os dados precisam ser editáveis. Portanto, eles precisam ser private
e var
. Fora do ViewModel
, os dados precisam ser legíveis, mas não editáveis, portanto, os dados precisam ser expostos como public
e val
. Para criar esse comportamento, o Kotlin oferece um recurso conhecido como propriedade de apoio (link em inglês).
Propriedade de apoio
Uma propriedade de apoio permite que você retorne algo de um getter diferente do objeto exato.
Você já sabe que, para cada propriedade, o framework do Kotlin gera getters e setters.
Para os métodos getter e setter, é possível substituir um desses métodos, ou ambos, e fornecer um comportamento personalizado próprio. Para implementar uma propriedade de apoio, você substituirá o método getter para retornar uma versão somente leitura dos dados. Exemplo de uma propriedade de apoio:
// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0
// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
get() = _count
Considere este exemplo. Os dados do seu app devem ser particulares para o ViewModel
:
Na classe ViewModel
:
- A propriedade
_count
éprivate
e mutável. Portanto, ela só pode ser acessada e editada na classeViewModel
. A convenção é prefixar a propriedadeprivate
com um sublinhado.
Fora da classe ViewModel
:
- O modificador de visibilidade padrão no Kotlin é
public
. Portanto, a classecount
é pública e pode ser acessada por outras classes, como controladores de IU. Como apenas o métodoget()
está sendo substituído, essa propriedade é imutável e somente leitura. Quando uma classe externa acessar essa propriedade, ela retornará o valor de_count
que não pode ser modificado. Isso protege os dados do app noViewModel
contra mudanças indesejadas e não seguras por classes externas, mas permite que os autores de chamadas externas acessem o valor com segurança.
Adicionar uma propriedade de apoio à currentScrambledWord
- No
GameViewModel
, mude a declaraçãocurrentScrambledWord
para adicionar uma propriedade de apoio. Agora a_currentScrambledWord
pode ser acessada e editada somente noGameViewModel
. O controlador de IUGameFragment
pode ler o valor dela usando a propriedade somente leitura,currentScrambledWord
.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
- No
GameFragment
, atualize o métodoupdateNextWordOnScreen()
para usar a propriedadeviewModel
somente leitura,currentScrambledWord
.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
- No
GameFragment
, exclua o código dos métodosonSubmitWord()
eonSkipWord()
. Você implementará esses métodos mais tarde. Agora, você pode compilar o código sem erros.
6. O ciclo de vida de um ViewModel
O framework mantém o ViewModel
ativo, desde que o escopo da atividade ou do fragmento esteja ativo. Um ViewModel
não será destruído se o proprietário for destruído durante uma mudança de configuração, como a rotação da tela. A nova instância do proprietário vai se reconectar à instância ViewModel
existente, conforme ilustrado por este diagrama:
Entender o ciclo de vida do ViewModel
Adicione a geração de registros no GameViewModel
e no GameFragment
para entender melhor o ciclo de vida do ViewModel
.
- No arquivo
GameViewModel.kt
, adicione um blocoinit
com um log statement.
class GameViewModel : ViewModel() {
init {
Log.d("GameFragment", "GameViewModel created!")
}
...
}
O Kotlin fornece o bloco inicializador, também conhecido como o bloco init
. O código de configuração inicial, necessário durante a inicialização de uma instância de objeto, ficará armazenado nesse bloco. Os blocos do inicializador são prefixados pela palavra-chave init
seguida por chaves {}
. Esse bloco de código é executado quando a instância do objeto é criada e inicializada pela primeira vez.
- Na classe
GameViewModel
, substitua o métodoonCleared()
. OViewModel
é destruído quando o fragmento associado é desanexado ou quando a atividade é concluída. Logo antes doViewModel
ser destruído, o callbackonCleared()
é chamado. - Adicione um log statement ao método
onCleared()
para monitorar o ciclo de vida doGameViewModel
.
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
- No
GameFragment
dentro deonCreateView()
, depois de receber uma referência para o objeto de vinculação, adicione um log statement para registrar a criação do fragmento. O callbackonCreateView()
será acionado quando o fragmento for criado pela primeira vez e sempre que ele for recriado para eventos como mudanças de configuração.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
return binding.root
}
- No
GameFragment
, substitua o método de callbackonDetach()
, que será chamado quando a atividade e o fragmento correspondentes forem destruídos.
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
- No Android Studio, execute o app, abra a janela Logcat e filtre por
GameFragment
.GameFragment
eGameViewModel
serão criados.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created!
- 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 que a tela gira, mas oGameViewModel
é criado apenas uma vez e não é recriado nem destruído para cada chamada.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created!
- Saia do jogo ou saia do app usando a seta para voltar. O
GameViewModel
será destruído e o callbackonCleared()
será chamado. OGameFragment
será destruído.
com.example.android.unscramble D/GameFragment: GameViewModel destroyed! com.example.android.unscramble D/GameFragment: GameFragment destroyed!
7. Preencher o ViewModel
Nesta tarefa, você preencherá ainda mais o GameViewModel
com métodos auxiliares para acessar a próxima palavra, validar a palavra do jogador para aumentar a pontuação e verificar a contagem de palavras para finalizar o jogo.
Inicialização atrasada
Normalmente, ao declarar uma variável, você fornece um valor inicial antecipadamente. No entanto, se ainda não quiser atribuir um valor, a inicialização poderá ser feita mais tarde. Para isso, use a palavra-chave lateinit
, que significa inicialização atrasada. Se você tiver certeza que a propriedade será inicializada antes de usá-la, poderá declarar a propriedade usando lateinit
. A memória não será alocada para a variável até que ela seja inicializada. Se você tentar acessar a variável antes de inicializá-la, o app falhará.
Acessar a próxima palavra
Crie o método getNextWord()
na classe GameViewModel
com esta funcionalidade:
- Acesse uma palavra aleatória da
allWordsList
e atribua àcurrentWord.
. - Embaralhe as letras na
currentWord
e atribua o resultado àcurrentScrambledWord
para criar uma palavra embaralhada. - Lide com o caso em que a palavra embaralhada é igual à palavra não embaralhada.
- Não mostre a mesma palavra duas vezes durante a partida.
Implemente estas etapas na classe GameViewModel
:
- No
GameViewModel,
, adicione uma nova variável de classe do tipoMutableList<String>
com o nomewordsList
para armazenar uma lista de palavras que você usará no jogo para evitar repetições. - Adicione outra variável de classe com o nome
currentWord
para armazenar a palavra que o jogador está tentando decifrar. Use a palavra-chavelateinit
, já que você vai inicializar esta propriedade mais tarde.
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
- Adicione um novo método
private
, sem parâmetros e que não retornará nada, com o nomegetNextWord()
e acima do blocoinit
. - Extraia uma palavra aleatória da
allWordsList
e a atribua àcurrentWord
.
private fun getNextWord() {
currentWord = allWordsList.random()
}
- No método
getNextWord()
, converta a stringcurrentWord
em uma matriz de caracteres e atribua ela a uma novaval
com o nometempWord
. Embaralhe os caracteres nesta matriz usando o método do Kotlin,shuffle()
(link em inglês) para criar a palavra embaralhada.
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
Uma Array
é semelhante a uma MutableList
, mas tem um tamanho fixo quando é inicializada. Uma Array
não pode aumentar nem diminuir de tamanho, é necessário copiar uma matriz para fazer o redimensionamento. Uma MutableList
, por outro lado, tem as funções add()
e remove()
e pode aumentar e diminuir de tamanho.
- Algumas vezes, a ordem aleatória de caracteres será igual a palavra original. Adicione esta repetição
while
na chamada para embaralhar a palavra para continuar a repetição até que a palavra embaralhada não seja igual à palavra original.
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
- Adicione um bloco
if-else
para verificar se uma palavra já foi usada. Se awordsList
contivercurrentWord
, chamegetNextWord()
. Caso contrário, atualize o valor da_currentScrambledWord
com a palavra recém embaralhada, aumente a contagem de palavras e adicione a nova palavra àwordsList
.
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
- Este é o método
getNextWord()
completo da sua referência.
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++currentWordCount
wordsList.add(currentWord)
}
}
Inicialização atrasada da currentScrambledWord
Agora, você criou o método getNextWord()
para acessar a próxima palavra. Ele será chamado quando o GameViewModel
for inicializado pela primeira vez. Use o bloco init
para inicializar propriedades lateinit
na classe, como a palavra atual. O resultado é que a primeira palavra exibida na tela será uma palavra embaralhada em vez de test.
- Execute o app. A primeira palavra sempre será "test".
- Para exibir uma palavra embaralhada ao iniciar o app, você precisa chamar o método
getNextWord()
, que atualizará acurrentScrambledWord
. Chame o métodogetNextWord()
no blocoinit
doGameViewModel
.
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
- Adicione o modificador
lateinit
à propriedade_currentScrambledWord
. Adicione uma referência explícita do tipo de dadosString
, já que nenhum valor inicial é fornecido.
private lateinit var _currentScrambledWord: String
- Execute o app. Observe que uma nova palavra embaralhada será exibida na inicialização do app. Incrível!
Adicionar um método auxiliar
Em seguida, adicione um método auxiliar para processar e modificar os dados no ViewModel
. Você usará esse método em tarefas futuras de codelab.
- Na classe
GameViewModel
, adicione outro método com o nomenextWord().
. Acesse a próxima palavra da lista e retornetrue
se a contagem de palavras for menor que oMAX_NO_OF_WORDS
.
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS.
* Updates the next word.
*/
fun nextWord(): Boolean {
return if (currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
8. Caixas de diálogo
No código inicial, a partida nunca acabava, mesmo após 10 palavras. Modifique o app para que, após o usuário passar por 10 palavras, a partida termine exibindo uma caixa de diálogo com a pontuação final. Também é possível oferecer ao usuário a opção de jogar novamente ou sair do jogo.
Esta é a primeira vez que você vai adicionar uma caixa de diálogo a um app. As caixas de diálogo são pequenas janelas (telas) que pedem para o usuário tomar uma decisão ou fornecer mais informações. Normalmente, uma caixa de diálogo não preenche toda a tela e exige que os usuários realizem uma ação antes de continuar. O Android oferece diferentes tipos de caixas de diálogo. Neste codelab, você vai aprender sobre caixas de diálogo de aviso.
Anatomia de uma caixa de diálogo de alerta
- Caixa de diálogo de aviso
- Título (opcional)
- Mensagem
- Botões de texto
Implementar a caixa de diálogo da pontuação final
Use a MaterialAlertDialog
(link em inglês) da biblioteca de componentes do Material Design para adicionar uma caixa de diálogo ao seu app que siga as diretrizes do Material Design. Como a caixa de diálogo está relacionada à IU, o GameFragment
será responsável por criar e exibir a caixa de diálogo da pontuação final.
- Primeiro, adicione uma propriedade de apoio à variável
score
. NoGameViewModel
, mude a declaração da variávelscore
para esta.
private var _score = 0
val score: Int
get() = _score
- No
GameFragment
, adicione uma função particular com o nomeshowFinalScoreDialog()
. Para criar umaMaterialAlertDialog
, use a classeMaterialAlertDialogBuilder
para juntar cada parte da caixa de diálogo. Chame o construtorMaterialAlertDialogBuilder
transmitindo o conteúdo usando o métodorequireContext()
do fragmento. O métodorequireContext()
retorna umContext
não nulo.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
}
Como o nome sugere, Context
se refere ao contexto ou ao estado atual de um aplicativo, uma atividade ou um fragmento. Ele contém as informações relacionadas à atividade, ao fragmento ou ao app. Geralmente, ele é usado para acessar recursos, bancos de dados e outros serviços do sistema. Nesta etapa, você transmitirá o contexto do fragmento para criar a caixa de diálogo de aviso.
Se solicitado pelo Android Studio, import
com.google.android.material.dialog.MaterialAlertDialogBuilder
.
- Adicione o código para definir o título na caixa de diálogo de aviso e use um recurso de string do arquivo
strings.xml
.
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
- Defina a mensagem para mostrar a pontuação final e use a versão somente leitura da variável correspondente
viewModel.score
que você adicionou anteriormente.
.setMessage(getString(R.string.you_scored, viewModel.score))
- Faça com que não seja possível cancelar a caixa de diálogo de aviso quando a tecla "Voltar" for pressionada usando o método
setCancelable()
e transmitindofalse
.
.setCancelable(false)
- Adicione dois botões de texto SAIR e JOGAR NOVAMENTE usando os métodos
setNegativeButton()
esetPositiveButton()
. ChameexitGame()
erestartGame()
, respectivamente, nos lambdas.
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
Essa sintaxe pode ser novidade para você, mas é uma abreviação de setNegativeButton(getString(R.string.exit), { _, _ -> exitGame()})
em que o método setNegativeButton()
recebe dois parâmetros: uma String
e uma função, DialogInterface.OnClickListener()
, que podem ser expressos como lambdas. Quando o último argumento transmitido é uma função, você pode colocar a expressão lambda fora dos parênteses. Isso é conhecido como sintaxe de lambda final (link em inglês). Ambas as formas de escrever o código (com o lambda dentro ou fora dos parênteses) são aceitáveis. O mesmo se aplica à função setPositiveButton
.
- No final, adicione o método
show()
, que cria e exibe a caixa de diálogo de aviso.
.show()
- Este é o método
showFinalScoreDialog()
completo para referência.
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
9. Implementar OnClickListener para botão "Submit"
Nesta tarefa, você usa o ViewModel
e a caixa de diálogo de aviso que adicionou para implementar a lógica do jogo para o listener de clique do botão Submit.
Exibir as palavras embaralhadas
- Caso ainda não tenha feito isso, no
GameFragment
, exclua o código do métodoonSubmitWord()
, que é chamado quando o usuário toca no botão Submit. - Adicione uma verificação para o valor de retorno do método
viewModel.nextWord()
. Se o valor fortrue
, outra palavra estará disponível, portanto, atualize a palavra embaralhada exibida na tela usandoupdateNextWordOnScreen()
. Caso contrário, a partida terminou, portanto, exiba a caixa de diálogo de aviso com a pontuação final.
private fun onSubmitWord() {
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- Execute o app. Jogue uma partida com algumas palavras. Lembre-se de que você ainda não implementou o botão Skip, por isso não é possível pular a palavra.
- O campo de texto não é atualizado, por isso o jogador precisa excluir manualmente a palavra anterior. A pontuação final na caixa de diálogo de aviso será sempre zero. Você corrigirá esses bugs nas próximas etapas.
Adicionar um método auxiliar para validar a palavra do jogador
- No
GameViewModel
, adicione um novo método particular com o nomeincreaseScore()
sem parâmetros e nenhum valor de retorno. Aumente a variávelscore
usandoSCORE_INCREASE
.
private fun increaseScore() {
_score += SCORE_INCREASE
}
- No método
GameViewModel
, adicione um método auxiliar com o nomeisUserWordCorrect()
que retorna umBoolean
e recebe umaString
, a palavra do jogador, como um parâmetro. - No método
isUserWordCorrect()
, valide a palavra do jogador e aumente a pontuação se o palpite estiver correto. Isso atualizará a pontuação final na caixa de diálogo de aviso.
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
Atualizar o campo de texto
Mostrar erros no campo de texto
Para campos de texto do Material Design, o TextInputLayout
oferece uma funcionalidade integrada para exibir mensagens de erro. Por exemplo, no campo de texto a seguir, a cor da etiqueta muda, um ícone de erro é mostrado, uma mensagem de erro aparece e assim por diante.
Para mostrar um erro no campo de texto, defina a mensagem de erro dinamicamente no código ou estaticamente no arquivo de layout. Confira abaixo um exemplo para definir e redefinir o erro no código:
// Set error text
passwordLayout.error = getString(R.string.error)
// Clear error text
passwordLayout.error = null
No código inicial, o método auxiliar setErrorTextField(error: Boolean)
já está configurado para ajudar a definir e redefinir o erro no campo de texto. Chame esse método usando true
ou false
como o parâmetro de entrada se você quiser ou não que um erro seja exibido no campo de texto.
Snippet de código no código inicial
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
Nesta tarefa, você implementará o método onSubmitWord()
. Quando uma palavra for enviada, valide o palpite do usuário verificando a palavra original. Se a palavra estiver correta, vá para a próxima palavra (ou mostre a caixa de diálogo se a partida tiver terminado). Se a palavra estiver incorreta, mostre um erro no campo de texto e continue na palavra atual.
- No
GameFragment,
no início do métodoonSubmitWord()
, crie umaval
com o nomeplayerWord
. Armazene a palavra do jogador extraindo-a do campo de texto da variávelbinding
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
...
}
- No método
onSubmitWord()
, abaixo da declaração daplayerWord
, valide a palavra do jogador. Adicione uma instruçãoif
para verificar a palavra do jogador usando o métodoisUserWordCorrect()
e transmitindo aplayerWord
. - No bloco
if
, redefina o campo de texto e chamesetErrorTextField
transmitindofalse
. - Mova o código existente para o bloco
if
.
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
}
- Se a palavra do usuário estiver incorreta, mostre uma mensagem de erro no campo de texto. Adicione um bloco
else
ao blocoif
acima e chame o métodosetErrorTextField()
transmitindotrue
. O métodoonSubmitWord()
concluído vai ficar assim:
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
- Execute o app. Jogue uma partida com algumas palavras. Se a palavra do jogador estiver correta, ela será apagada ao clicar no botão Submit, caso contrário, uma mensagem "Try again!" será exibida para que o jogador possa tentar novamente. O botão Skip ainda não funciona. Você adicionará essa implementação na próxima tarefa.
10. Implementar o botão "Skip"
Nesta tarefa, você vai implementar o método onSkipWord()
que processa o clique do botão Skip.
- Semelhante ao método
onSubmitWord()
, adicione uma condição no métodoonSkipWord()
. Se o valor retornado fortrue
, exiba a palavra na tela e redefina o campo de texto. Se o valor retornado forfalse
e não houver mais palavras nesta partida, mostre a caixa de diálogo de aviso com a pontuação final.
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
- Execute o app. Jogue uma partida. Observe que os botões Skip e Submit estão funcionando como esperado. Excelente!
11. Verificar se o ViewModel preserva dados
Nesta tarefa, adicione a geração de registros ao GameFragment
para observar que os dados do app são preservados no ViewModel
durante as mudanças na configuração. Para acessar a currentWordCount
no GameFragment
, é necessário expor uma versão somente leitura usando uma propriedade de apoio.
- No
GameViewModel
, clique com o botão direito do mouse na variávelcurrentWordCount
, selecione Refactor > Rename... . Adicione um sublinhado ao novo nome:_currentWordCount
. - Adicione um campo de apoio.
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
- No
GameFragment
no métodoonCreateView()
, acima da instrução de retorno, adicione outro registro para exibir os dados do app, a palavra, a pontuação e a contagem de palavras.
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
- No Android Studio, abra o Logcat e filtre por
GameFragment
. Execute o app e jogue uma partida com algumas palavras. Mude a orientação do dispositivo. O fragmento (controlador de IU) será destruído e recriado. Confira os registros. Agora, a pontuação e a contagem de palavras vão aumentar.
com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: GameViewModel created! com.example.android.unscramble D/GameFragment: Word: oimfnru Score: 0 WordCount: 1 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: ofx Score: 80 WordCount: 5 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9 com.example.android.unscramble D/GameFragment: GameFragment destroyed! com.example.android.unscramble D/GameFragment: GameFragment created/re-created! com.example.android.unscramble D/GameFragment: Word: nvoiil Score: 160 WordCount: 9
Os dados do app são preservados no ViewModel
durante as mudanças de orientação. Você atualizará o valor da pontuação e a contagem de palavras na IU usando o LiveData
e a vinculação de dados nos próximos codelabs.
12. Atualizar lógica de reinicialização do jogo
- Execute o app novamente, jogue até o final. Na caixa de diálogo de aviso Congratulations!, clique em PLAY AGAIN para jogar novamente. O app não deixará você jogar novamente porque o número de palavras atingiu o valor
MAX_NO_OF_WORDS
. Você precisará redefinir a contagem de palavras para 0 para jogar de novo desde o começo. - Para redefinir os dados do app, no
GameViewModel
adicione um método com o nomereinitializeData()
. Defina a pontuação e a contagem de palavras como0
. Limpe a lista de palavras e chame o métodogetNextWord()
.
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
- No
GameFragment
na parte de cima do métodorestartGame()
, chame o método recém-criado,reinitializeData()
.
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
- Execute o app novamente. Jogue uma partida. Na caixa de diálogo de parabéns, clique em Play again. Agora você conseguirá jogar novamente.
O app final funcionará assim. O jogo mostrará dez palavras aleatórias embaralhadas para o jogador decifrar. Você pode pular a palavra tocando no botão Skip ou tentar adivinhá-la tocando no botão Submit. Caso seu palpite esteja correto, a pontuação aumentará. Um palpite incorreto mostrará um estado de erro no campo de texto. A cada nova palavra, a contagem de palavras também aumentará.
A pontuação e a contagem de palavras exibidas na tela ainda não serão atualizadas. No entanto, as informações ainda estão sendo armazenadas no modelo de visualização e preservadas durante as mudanças de configuração, como a rotação do dispositivo. Você vai atualizar a pontuação e a contagem de palavras na tela nos próximos codelabs.
Ao final de 10 palavras, a partida acabará, e uma caixa de diálogo de aviso será exibida com a pontuação final, além de uma opção para sair do jogo ou jogar novamente.
Parabéns! Você criou seu primeiro ViewModel
e salvou os dados.
13. Código da solução
GameFragment.kt
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.example.android.unscramble.R
import com.example.android.unscramble.databinding.GameFragmentBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Fragment where the game is played, contains the game logic.
*/
class GameFragment : Fragment() {
private val viewModel: GameViewModel by viewModels()
// Binding object instance with access to the views in the game_fragment.xml layout
private lateinit var binding: GameFragmentBinding
// Create a ViewModel the first time the fragment is created.
// If the fragment is re-created, it receives the same GameViewModel instance created by the
// first fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout XML file and return a binding object instance
binding = GameFragmentBinding.inflate(inflater, container, false)
Log.d("GameFragment", "GameFragment created/re-created!")
Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
"Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Setup a click listener for the Submit and Skip buttons.
binding.submit.setOnClickListener { onSubmitWord() }
binding.skip.setOnClickListener { onSkipWord() }
// Update the UI
updateNextWordOnScreen()
binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(
R.string.word_count, 0, MAX_NO_OF_WORDS)
}
/*
* Checks the user's word, and updates the score accordingly.
* Displays the next scrambled word.
* After the last word, the user is shown a Dialog with the final score.
*/
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (viewModel.nextWord()) {
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
/*
* Skips the current word without changing the score.
*/
private fun onSkipWord() {
if (viewModel.nextWord()) {
setErrorTextField(false)
updateNextWordOnScreen()
} else {
showFinalScoreDialog()
}
}
/*
* Gets a random word for the list of words and shuffles the letters in it.
*/
private fun getNextScrambledWord(): String {
val tempWord = allWordsList.random().toCharArray()
tempWord.shuffle()
return String(tempWord)
}
/*
* Creates and shows an AlertDialog with the final score.
*/
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
/*
* Re-initializes the data in the ViewModel and updates the views with the new data, to
* restart the game.
*/
private fun restartGame() {
viewModel.reinitializeData()
setErrorTextField(false)
updateNextWordOnScreen()
}
/*
* Exits the game.
*/
private fun exitGame() {
activity?.finish()
}
override fun onDetach() {
super.onDetach()
Log.d("GameFragment", "GameFragment destroyed!")
}
/*
* Sets and resets the text field error status.
*/
private fun setErrorTextField(error: Boolean) {
if (error) {
binding.textField.isErrorEnabled = true
binding.textField.error = getString(R.string.try_again)
} else {
binding.textField.isErrorEnabled = false
binding.textInputEditText.text = null
}
}
/*
* Displays the next scrambled word on screen.
*/
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
}
GameViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
/**
* ViewModel containing the app data and methods to process the data
*/
class GameViewModel : ViewModel(){
private var _score = 0
val score: Int
get() = _score
private var _currentWordCount = 0
val currentWordCount: Int
get() = _currentWordCount
private lateinit var _currentScrambledWord: String
val currentScrambledWord: String
get() = _currentScrambledWord
// List of words used in the game
private var wordsList: MutableList<String> = mutableListOf()
private lateinit var currentWord: String
init {
Log.d("GameFragment", "GameViewModel created!")
getNextWord()
}
override fun onCleared() {
super.onCleared()
Log.d("GameFragment", "GameViewModel destroyed!")
}
/*
* Updates currentWord and currentScrambledWord with the next word.
*/
private fun getNextWord() {
currentWord = allWordsList.random()
val tempWord = currentWord.toCharArray()
tempWord.shuffle()
while (String(tempWord).equals(currentWord, false)) {
tempWord.shuffle()
}
if (wordsList.contains(currentWord)) {
getNextWord()
} else {
_currentScrambledWord = String(tempWord)
++_currentWordCount
wordsList.add(currentWord)
}
}
/*
* Re-initializes the game data to restart the game.
*/
fun reinitializeData() {
_score = 0
_currentWordCount = 0
wordsList.clear()
getNextWord()
}
/*
* Increases the game score if the player's word is correct.
*/
private fun increaseScore() {
_score += SCORE_INCREASE
}
/*
* Returns true if the player word is correct.
* Increases the score accordingly.
*/
fun isUserWordCorrect(playerWord: String): Boolean {
if (playerWord.equals(currentWord, true)) {
increaseScore()
return true
}
return false
}
/*
* Returns true if the current word count is less than MAX_NO_OF_WORDS
*/
fun nextWord(): Boolean {
return if (_currentWordCount < MAX_NO_OF_WORDS) {
getNextWord()
true
} else false
}
}
14. Resumo
- As diretrizes de arquitetura de apps Android recomendam separar classes que tenham responsabilidades diferentes e usar um modelo para a IU.
- Um controlador de IU é uma classe com base na IU, como a
Activity
ou oFragment
. Os controladores de IU precisam conter somente a lógica que processa as interações entre a IU e o sistema operacional. Eles não podem ser a fonte de dados a ser exibida na IU. Armazene esses dados e qualquer lógica relacionada em umViewModel
. - A classe
ViewModel
armazena e gerencia dados relacionados à IU. A classeViewModel
permite que os dados sobrevivam às mudanças de configuração, como a rotação da tela. - O
ViewModel
é um dos componentes recomendados pelos Componentes da arquitetura do Android.
15. Saiba mais
- Visão geral do ViewModel
- Guia para a arquitetura do app
- Os componentes do Material Design para Android em prática: caixas de diálogo (links em inglês)
- Anatomia da caixa de diálogo de alerta (link em inglês)
- MaterialAlertDialogBuilder
- Propriedades de apoio (link em inglês)
- Componentes da arquitetura do Android
- Caixas de diálogo do Material Design para Android (link em inglês)
- Propriedades e campos: getters, setters, const e lateinit (link em inglês)