Este codelab faz parte do curso Conceitos básicos do Kotlin para Android. Você vai aproveitar mais este curso se fizer os codelabs em sequência. Todos os codelabs do curso estão listados na página inicial dos codelabs de princípios básicos do Kotlin para Android.
Introdução
Este codelab recapitula como usar ViewModel
e fragmentos juntos para implementar a navegação. O objetivo é colocar a lógica de when para navegar no ViewModel
, mas definir os caminhos nos fragmentos e no arquivo de navegação. Para atingir essa meta, use modelos de visualização, fragmentos, LiveData
e observadores.
O codelab conclui mostrando uma maneira inteligente de rastrear estados de botões com o mínimo de código, para que cada botão seja ativado e clicável apenas quando fizer sentido para o usuário tocar nele.
O que você já precisa saber
Você precisa:
- Criar uma interface do usuário (UI) básica usando uma atividade, fragmentos e visualizações.
- Navegar entre fragmentos e usar
safeArgs
para transmitir dados entre eles. - Ver modelos, fábricas de modelos, transformações,
LiveData
e os observadores deles. - Como criar um banco de dados
Room
, um objeto de acesso a dados (DAO) e definir entidades. - Como usar corrotinas para interações com bancos de dados e outras tarefas de longa duração.
O que você vai aprender
- Como atualizar um registro de qualidade do sono no banco de dados.
- Como usar
LiveData
para rastrear estados de botões. - Como mostrar uma snackbar em resposta a um evento.
Atividades deste laboratório
- Estenda o app TrackMySleepQuality para coletar uma classificação de qualidade, adicionar a avaliação ao banco de dados e mostrar o resultado.
- Use
LiveData
para acionar a exibição de uma snackbar. - Use
LiveData
para ativar e desativar botões.
Neste codelab, você vai criar a gravação da qualidade do sono e a interface finalizada do app TrackMySleepQuality.
O app tem duas telas, representadas por fragmentos, como mostrado na figura abaixo.
A primeira tela, mostrada à esquerda, tem botões para iniciar e parar o rastreamento. A tela mostra todos os dados de sono do usuário. O botão Limpar exclui permanentemente todos os dados que o app coletou do usuário.
A segunda tela, mostrada à direita, é para selecionar uma classificação de qualidade do sono. No app, a classificação é representada numericamente. Para fins de desenvolvimento, o app mostra os ícones de rosto e os equivalentes numéricos deles.
O fluxo do usuário é o seguinte:
- O usuário abre o app e a tela de monitoramento do sono aparece.
- O usuário toca no botão Iniciar. Isso registra e mostra o horário de início. O botão Iniciar está desativado, e o botão Parar está ativado.
- O usuário toca no botão Parar. Isso registra o horário de término e abre a tela de qualidade do sono.
- O usuário seleciona um ícone de qualidade do sono. A tela fecha, e a tela de monitoramento mostra a hora de fim do sono e a qualidade dele. O botão Parar está desativado, e o botão Iniciar está ativado. O app está pronto para outra noite.
- O botão Limpar fica ativado sempre que há dados no banco de dados. Quando o usuário toca no botão Limpar, todos os dados são apagados sem recurso. Não há uma mensagem "Tem certeza?".
Esse app usa uma arquitetura simplificada, conforme mostrado abaixo no contexto da arquitetura completa. O app usa apenas os seguintes componentes:
- Controlador de interface
- Mostrar modelo e
LiveData
- Um banco de dados do Room
Este codelab pressupõe que você sabe implementar a navegação usando fragmentos e o arquivo de navegação. Para facilitar seu trabalho, grande parte desse código é fornecida.
Etapa 1: inspecionar o código
- Para começar, continue com seu próprio código do final do último codelab ou baixe o código inicial.
- No código inicial, inspecione o
SleepQualityFragment
. Essa classe aumenta o layout, recebe o aplicativo e retornabinding.root
. - Abra navigation.xml no editor de design. Você vê que há um caminho de navegação de
SleepTrackerFragment
paraSleepQualityFragment
e de volta deSleepQualityFragment
paraSleepTrackerFragment
. - Inspecione o código de navigation.xml. Procure especificamente o
<argument>
chamadosleepNightKey
.
Quando o usuário vai deSleepTrackerFragment
paraSleepQualityFragment,
, o app transmite umsleepNightKey
para oSleepQualityFragment
da noite que precisa ser atualizada.
Etapa 2: adicionar navegação para o acompanhamento da qualidade do sono
O gráfico de navegação já inclui os caminhos do SleepTrackerFragment
para o SleepQualityFragment
e vice-versa. No entanto, os manipuladores de cliques que implementam a navegação de um fragmento para o próximo ainda não foram codificados. Adicione esse código agora no ViewModel
.
No manipulador de cliques, você define um LiveData
que muda quando você quer que o app navegue para um destino diferente. O fragmento observa esse LiveData
. Quando os dados mudam, o fragmento navega até o destino e informa ao modelo de visualização que a ação foi concluída, o que redefine a variável de estado.
- Abra
SleepTrackerViewModel
. Você precisa adicionar navegação para que, quando o usuário tocar no botão Parar, o app navegue atéSleepQualityFragment
para coletar uma classificação de qualidade. - Em
SleepTrackerViewModel
, crie umLiveData
que mude quando você quiser que o app navegue até oSleepQualityFragment
. Use o encapsulamento para expor apenas uma versão recuperável doLiveData
aoViewModel
.
Você pode colocar esse código em qualquer lugar no nível superior do corpo da classe.
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- Adicione uma função
doneNavigating()
que redefine a variável que aciona a navegação.
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- No gerenciador de cliques do botão Stop,
onStopTracking()
, acione a navegação atéSleepQualityFragment
. Defina a variável _navigateToSleepQuality
no final da função como a última coisa dentro do blocolaunch{}
. Essa variável está definida comonight
. Quando essa variável tem um valor, o app navega até oSleepQualityFragment
, transmitindo o night.
.
_navigateToSleepQuality.value = oldNight
- O
SleepTrackerFragment
precisa observar _navigateToSleepQuality
para que o app saiba quando navegar. NoSleepTrackerFragment
, emonCreateView()
, adicione um observador paranavigateToSleepQuality()
. A importação para isso é ambígua, e você precisa importarandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- No bloco do observador, navegue e transmita o ID da noite atual. Em seguida, chame
doneNavigating()
. Se a importação for ambígua, importeandroidx.navigation.fragment.findNavController
.
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- Compile e execute o app. Toque em Iniciar e em Parar, o que leva você à tela
SleepQualityFragment
. Para voltar, use o botão "Voltar" do sistema.
Nesta tarefa, você vai registrar a qualidade do sono e voltar ao fragmento do rastreador de sono. A tela será atualizada automaticamente para mostrar o valor atualizado ao usuário. Você precisa criar um ViewModel
e um ViewModelFactory
, além de atualizar o SleepQualityFragment
.
Etapa 1: criar um ViewModel e uma ViewModelFactory
- No pacote
sleepquality
, crie ou abra SleepQualityViewModel.kt. - Crie uma classe
SleepQualityViewModel
que receba umsleepNightKey
e um banco de dados como argumentos. Assim como fez com oSleepTrackerViewModel
, você precisa transmitir odatabase
da fábrica. Você também precisa transmitir osleepNightKey
da navegação.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
- Na classe
SleepQualityViewModel
, defina umJob
e umuiScope
, e substituaonCleared()
.
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- Para voltar ao
SleepTrackerFragment
usando o mesmo padrão acima, declare_navigateToSleepTracker
. ImplementenavigateToSleepTracker
edoneNavigating()
.
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- Crie um manipulador de cliques,
onSetSleepQuality()
, para todas as imagens de qualidade do sono a serem usadas.
Use o mesmo padrão de corrotina do codelab anterior:
- Inicie uma corrotina no
uiScope
e mude para o dispatcher de E/S. - Receba
tonight
usandosleepNightKey
. - Defina a qualidade do sono.
- Atualize o banco de dados.
- Acionar navegação.
O exemplo de código abaixo faz todo o trabalho no gerenciador de cliques, em vez de fatorar a operação do banco de dados no contexto diferente.
fun onSetSleepQuality(quality: Int) {
uiScope.launch {
// IO is a thread pool for running operations that access the disk, such as
// our Room database.
withContext(Dispatchers.IO) {
val tonight = database.get(sleepNightKey) ?: return@withContext
tonight.sleepQuality = quality
database.update(tonight)
}
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
- No pacote
sleepquality
, crie ou abraSleepQualityViewModelFactory.kt
e adicione a classeSleepQualityViewModelFactory
, conforme mostrado abaixo. Essa classe usa uma versão do mesmo código boilerplate que você já viu. Inspecione o código antes de continuar.
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Etapa 2: atualizar o SleepQualityFragment
- Abra
SleepQualityFragment.kt
. - Em
onCreateView()
, depois de receber oapplication
, você precisa receber oarguments
que veio com a navegação. Esses argumentos estão emSleepQualityFragmentArgs
. É necessário extraí-los do pacote.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- Em seguida, gere o
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Crie uma fábrica, transmitindo o
dataSource
e osleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Receba uma referência
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- Adicione o
ViewModel
ao objeto de vinculação. Se você encontrar um erro com o objeto de vinculação, ignore-o por enquanto.
binding.sleepQualityViewModel = sleepQualityViewModel
- Adicione o observador. Quando solicitado, importe
androidx.lifecycle.Observer
.
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
Etapa 3: atualizar o arquivo de layout e executar o app
- Abra o arquivo de layout
fragment_sleep_quality.xml
. No bloco<data>
, adicione uma variável para oSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- Para cada uma das seis imagens de qualidade do sono, adicione um manipulador de cliques como o abaixo. Faça a correspondência entre a classificação de qualidade e a imagem.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- Limpe e recrie o projeto. Isso vai resolver todos os erros com o objeto de vinculação. Caso contrário, limpe o cache (File > Invalidate Caches / Restart) e reconstrua o app.
Parabéns! Você acabou de criar um app de banco de dados Room
completo usando corrotinas.
Agora seu app funciona muito bem. O usuário pode tocar em Iniciar e Parar quantas vezes quiser. Quando o usuário toca em Parar, ele pode inserir uma qualidade do sono. Quando o usuário toca em Limpar, todos os dados são apagados silenciosamente em segundo plano. No entanto, todos os botões estão sempre ativados e clicáveis, o que não prejudica o app, mas permite que os usuários criem noites de sono incompletas.
Nesta última tarefa, você vai aprender a usar mapas de transformação para gerenciar a visibilidade dos botões e permitir que os usuários façam apenas a escolha certa. Você pode usar um método semelhante para mostrar uma mensagem amigável depois que todos os dados forem apagados.
Etapa 1: atualizar os estados dos botões
A ideia é definir o estado do botão para que, no início, apenas o botão Start esteja ativado, o que significa que ele pode ser clicado.
Depois que o usuário toca em Iniciar, o botão Parar é ativado, e o Iniciar não. O botão Limpar só é ativado quando há dados no banco de dados.
- Abra o arquivo de layout
fragment_sleep_tracker.xml
. - Adicione a propriedade
android:enabled
a cada botão. A propriedadeandroid:enabled
é um valor booleano que indica se o botão está ativado ou não. Um botão ativado pode ser tocado, mas um botão desativado não. Atribua à propriedade o valor de uma variável de estado que você vai definir em breve.
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
- Abra
SleepTrackerViewModel
e crie três variáveis correspondentes. Atribua a cada variável uma transformação que a teste.
- O botão Iniciar precisa ser ativado quando
tonight
fornull
. - O botão Parar precisa estar ativado quando
tonight
não fornull
. - O botão Limpar só deve ser ativado se
nights
e, portanto, o banco de dados contiver noites de sono.
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
- Execute o app e teste os botões.
Etapa 2: usar uma snackbar para notificar o usuário
Depois que o usuário limpar o banco de dados, mostre uma confirmação usando o widget Snackbar
. Um snackbar fornece um feedback rápido sobre uma operação por meio de uma mensagem na parte de baixo da tela. Uma snackbar desaparece após um tempo limite, depois de uma interação do usuário em outro lugar na tela ou depois que o usuário desliza a snackbar para fora da tela.
Mostrar a snackbar é uma tarefa da interface, e isso precisa acontecer no fragmento. A decisão de mostrar a snackbar acontece no ViewModel
. Para configurar e acionar uma snackbar quando os dados forem limpos, use a mesma técnica de acionamento da navegação.
- No
SleepTrackerViewModel
, crie o evento encapsulado.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Em seguida, implemente
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- No
SleepTrackerFragment
, emonCreateView()
, adicione um observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Dentro do bloco do observador, mostre a snackbar e redefina o evento imediatamente.
if (it == true) { // Observed state is true.
Snackbar.make(
activity!!.findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
- Em
SleepTrackerViewModel
, acione o evento no métodoonClear()
. Para isso, defina o valor do evento comotrue
dentro do blocolaunch
:
_showSnackbarEvent.value = true
- Crie e execute o app.
Projeto do Android Studio: TrackMySleepQualityFinal
Implementar o monitoramento da qualidade do sono nesse app é como tocar uma música conhecida em uma nova tonalidade. Embora os detalhes mudem, o padrão básico do que você fez em codelabs anteriores nesta lição permanece o mesmo. Conhecer esses padrões acelera muito a programação, porque é possível reutilizar o código de apps existentes. Confira alguns dos padrões usados neste curso até agora:
- Crie um
ViewModel
e umViewModelFactory
e configure uma fonte de dados. - Acionar navegação. Para separar as responsabilidades, coloque o manipulador de cliques no modelo de visualização e a navegação no fragmento.
- Use o encapsulamento com
LiveData
para rastrear e responder a mudanças de estado. - Use transformações com
LiveData
. - Crie um banco de dados singleton.
- Configure corrotinas para operações de banco de dados.
Acionamento da navegação
Você define possíveis caminhos de navegação entre fragmentos em um arquivo de navegação. Há algumas maneiras diferentes de acionar a navegação de um fragmento para o próximo. São eles:
- Defina gerenciadores
onClick
para acionar a navegação até um fragmento de destino. - Como alternativa, para ativar a navegação de um fragmento para o próximo:
- Defina um valor
LiveData
para registrar se a navegação deve ocorrer. - Anexe um observador a esse valor
LiveData
. - Em seguida, seu código muda esse valor sempre que a navegação precisa ser acionada ou é concluída.
Definir o atributo android:enabled
- O atributo
android:enabled
é definido emTextView
e herdado por todas as subclasses, incluindoButton
. - O atributo
android:enabled
determina se umView
está ativado ou não. O significado de "ativado" varia de acordo com a subclasse. Por exemplo, umEditText
não ativado impede que o usuário edite o texto contido, e umButton
não ativado impede que o usuário toque no botão. - O atributo
enabled
não é o mesmo que o atributovisibility
. - É possível usar mapas de transformação para definir o valor do atributo
enabled
de botões com base no estado de outro objeto ou variável.
Outros pontos abordados neste codelab:
- Para acionar notificações ao usuário, use a mesma técnica que você usa para acionar a navegação.
- Você pode usar um
Snackbar
para notificar o usuário.
Curso da Udacity:
Documentação do desenvolvedor Android:
Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:
- Atribuir o dever de casa, se necessário.
- Informar aos alunos como enviar deveres de casa.
- Atribuir nota aos deveres de casa.
Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.
Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.
Responda estas perguntas
Pergunta 1
Uma maneira de permitir que seu app acione a navegação de um fragmento para o próximo é usar um valor LiveData
para indicar se a navegação deve ser acionada ou não.
Quais são as etapas para usar um valor LiveData
, chamado gotoBlueFragment
, para acionar a navegação do fragmento vermelho para o azul? Selecione todas as opções válidas.
- Em
ViewModel
, defina o valorgotoBlueFragment
deLiveData
. - Em
RedFragment
, observe o valorgotoBlueFragment
. Implemente o códigoobserve{}
para navegar atéBlueFragment
quando apropriado e, em seguida, redefina o valor degotoBlueFragment
para indicar que a navegação foi concluída. - Verifique se o código define a variável
gotoBlueFragment
como o valor que aciona a navegação sempre que o app precisa ir deRedFragment
paraBlueFragment
. - Verifique se o código define um manipulador
onClick
para oView
em que o usuário clica para navegar atéBlueFragment
, em que o manipuladoronClick
observa o valorgoToBlueFragment
.
Pergunta 2
É possível mudar se um Button
está ativado (clicável) ou não usando LiveData
. Como você garantiria que o app mudasse o botão UpdateNumber
para que:
- O botão é ativado se
myNumber
tiver um valor maior que 5. - O botão não fica ativado se
myNumber
for igual ou menor que 5.
Suponha que o layout que contém o botão UpdateNumber
inclua a variável <data>
para o NumbersViewModel
, conforme mostrado aqui:
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
Suponha que o ID do botão no arquivo de layout seja o seguinte:
android:id="@+id/update_number_button"
O que mais você precisa fazer? Selecione todas as opções aplicáveis.
- Na classe
NumbersViewModel
, defina uma variávelLiveData
,myNumber
, que representa o número. Defina também uma variável cujo valor é definido chamandoTransform.map()
na variávelmyNumber
, que retorna um booleano indicando se o número é maior que 5 ou não.
Especificamente, emViewModel
, adicione o seguinte código:
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- No layout XML, defina o atributo
android:enabled
doupdate_number_button button
comoNumberViewModel.enableUpdateNumbersButton
.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- No
Fragment
que usa a classeNumbersViewModel
, adicione um observador ao atributoenabled
do botão.
Especificamente, emFragment
, adicione o seguinte código:
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
myNumber > 5
})
- No arquivo de layout, defina o atributo
android:enabled
doupdate_number_button button
como"Observable"
.
Comece a próxima lição:
Para acessar links de outros codelabs neste curso, consulte a página inicial dos codelabs de conceitos básicos do Kotlin para Android.