Este codelab faz parte do curso Conceitos básicos do Kotlin para Android. Você aproveitará mais o curso se fizer os codelabs em sequência. Todos os codelabs do curso estão listados na página de destino dos codelabs do curso Conceitos básicos do Kotlin para Android.
Introdução
Este codelab resume como usar ViewModel
e fragmentos juntos para implementar a navegação. O objetivo é colocar a lógica de when para navegar até o ViewModel
, mas definir os caminhos nos fragmentos e no arquivo de navegação. Para atingir essa meta, você usa modelos de visualização, fragmentos, LiveData
e observadores.
O codelab termina mostrando uma maneira inteligente de rastrear os estados dos botões com o mínimo de código para que cada um deles seja ativado e clicável somente quando fizer sentido para o usuário tocar nesse botão.
O que você já precisa saber
Você precisa:
- Criar uma interface do usuário (IU) básica usando uma atividade, fragmentos e visualizações.
- Navegação entre fragmentos e uso de
safeArgs
para transmitir dados entre fragmentos. - Veja modelos, visualizações de fábricas, transformações e
LiveData
e os respectivos observadores. - Como criar um banco de dados
Room
, um objeto de acesso a dados (DAO, na sigla em inglês) e definir entidades. - Como usar corrotinas para interações de banco de dados e outras tarefas de longa duração.
O que você vai aprender
- Como atualizar um registro de qualidade do sono existente no banco de dados.
- Como usar
LiveData
para rastrear estados de botões. - Como exibir uma snackbar em resposta a um evento.
Atividades do laboratório
- Estenda o app TrackMySleepQuality para coletar uma classificação de qualidade, adicioná-la ao banco de dados e exibir o resultado.
- Use
LiveData
para acionar a exibição de um snackbar. - Use
LiveData
para ativar e desativar botões.
Neste codelab, você vai criar a IU com qualidade de sono e a IU final 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 interromper 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 para o usuário.
A segunda tela, mostrada à direita, serve 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 rostos e os equivalentes numéricos deles.
O fluxo do usuário é o seguinte:
- O usuário abre o app e vê a tela de monitoramento do sono.
- O usuário toca no botão Iniciar. Ele registra o horário de início e o exibe. O botão Iniciar está desativado, e o botão Parar está ativado.
- O usuário toca no botão Parar. 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 será fechada, e a tela de monitoramento exibirá a hora de término do sono e a qualidade do sono. O botão Parar está desativado, e o botão Iniciar está ativado. O app está pronto para outra noite.
- O botão Limpar será ativado sempre que houver dados no banco de dados. Quando o usuário toca no botão Limpar, todos os dados dele são apagados sem fazer o recurso, e não há a mensagem "Você 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 IU
- Ver modelo e
LiveData
- Um banco de dados da Room
Este codelab pressupõe que você sabe implementar a navegação usando fragmentos e o arquivo de navegação. Para salvar seu trabalho, uma grande parte desse código é fornecida.
Etapa 1: inspecionar o código
- Para começar, continue com seu próprio código do fim do último codelab ou faça o download do código inicial.
- No código inicial, inspecione o
SleepQualityFragment
. Essa classe infla o layout, recebe o app e retornabinding.root
. - Abra o arquivo navigation.xml no editor de design. Há um caminho de navegação do
SleepTrackerFragment
até oSleepQualityFragment
, e vice-versa.
. - Inspecione o código do navigation.xml. Procure pelo
<argument>
chamadosleepNightKey
.
Quando o usuário vai doSleepTrackerFragment
para oSleepQualityFragment,
, o app transmite umsleepNightKey
para oSleepQualityFragment
da noite que precisa ser atualizado.
Etapa 2: adicionar navegação para monitorar a qualidade do sono
O gráfico de navegação já inclui os caminhos de SleepTrackerFragment
para SleepQualityFragment
e vice-versa. No entanto, os gerenciadores de cliques que implementam a navegação de um fragmento para o próximo ainda não estão codificados. Adicione esse código agora no ViewModel
.
No gerenciador de cliques, defina uma LiveData
que muda quando você quer que o app navegue para um destino diferente. O fragmento observa esse LiveData
. Quando os dados são modificados, o fragmento navega até o destino e informa ao modelo de visualização que ele foi concluído, o que redefine a variável de estado.
- Abra o
SleepTrackerViewModel
É necessário adicionar navegação para que, quando o usuário tocar no botão Stop, 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 para aSleepQualityFragment
. Use o encapsulamento para expor apenas uma versão getable doLiveData
para oViewModel
.
Você pode colocar esse código em qualquer lugar na parte superior do corpo da turma.
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 para oSleepQualityFragment
. Defina a variável _navigateToSleepQuality
no final da função como a última coisa dentro do blocolaunch{}
. Essa variável é definida comonight
. Quando essa variável tiver um valor, o app navegará para oSleepQualityFragment
, transmitindo durante a noite.
_navigateToSleepQuality.value = oldNight
- O
SleepTrackerFragment
precisa observar _navigateToSleepQuality
para que o app saiba quando navegar. NoSleepTrackerFragment
, emonCreateView()
, adicione um observador para anavigateToSleepQuality()
. A importação para isso é ambígua, e você precisa importarandroidx.lifecycle.Observer
.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- No bloco de observadores, navegue e transmita o ID da noite atual e, em seguida, chame
doneNavigating()
. Se sua 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 para acessar a 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 tracker de sono. A exibição será atualizada automaticamente para mostrar o valor atualizado ao usuário. Você precisa criar um ViewModel
e um ViewModelFactory
e atualizar a SleepQualityFragment
.
Etapa 1: criar um ViewModel e um ViewModelFactory
- No pacote
sleepquality
, crie ou abra SleepQualityViewModel.kt. - Crie uma classe
SleepQualityViewModel
que recebe umsleepNightKey
e um banco de dados como argumentos. Assim como foi feito noSleepTrackerViewModel
, 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 modifiqueonCleared()
.
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 gerenciador de cliques,
onSetSleepQuality()
, para que todas as imagens com qualidade de sono sejam usadas.
Use o mesmo padrão de corrotina do codelab anterior:
- Inicie uma corrotina no
uiScope
e alterne para o agente de E/S. - Acesse
tonight
usando osleepNightKey
. - Defina a qualidade do sono.
- Atualize o banco de dados.
- Acione a navegação.
O exemplo de código abaixo faz todo o trabalho no gerenciador de clique, em vez de considerar a operação do banco de dados em um 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. Esta classe usa uma versão do mesmo código padrão 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 o
SleepQualityFragment.kt
- Em
onCreateView()
, depois de receber oapplication
, você precisará doarguments
que veio com a navegação. Esses argumentos estão emSleepQualityFragmentArgs
. Você precisa extraí-los do pacote.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- Em seguida, acesse o
dataSource
.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
- Crie uma fábrica, transmitindo o
dataSource
e asleepNightKey
.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
- Receber uma referência
ViewModel
.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- Adicione
ViewModel
ao objeto de vinculação. Se você vir 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 aSleepQualityViewModel
.
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- Para cada uma das seis imagens com qualidade de sono, adicione um gerenciador de cliques como o mostrado abaixo. Associe a classificação de qualidade à imagem.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- Limpe e recrie seu projeto. Isso deve resolver os erros com o objeto de vinculação. Caso contrário, limpe o cache (File > Invalidate Caches / Restart) e recrie seu aplicativo.
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 de sono. Quando o usuário toca em Limpar, todos os dados são apagados de maneira silenciosa em segundo plano. No entanto, todos os botões são sempre ativados e clicáveis, o que não interrompe o app. No entanto, ele permite que os usuários criem noites de sono incompletas.
Nesta última tarefa, você aprenderá a usar mapas de transformação para gerenciar a visibilidade do botão. Assim, os usuários poderão fazer a escolha certa. Você pode usar um método semelhante para exibir uma mensagem amigável depois que todos os dados forem apagados.
Etapa 1: atualizar os estados do botão
A ideia é definir o estado para que, no início, apenas o botão Start seja ativado, o que significa que é clicável.
Depois que o usuário tocar em Iniciar, o botão Parar será ativado, e Iniciar não. O botão Limpar só será ativado se houver 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. É possível tocar em um botão ativado. Em um botão desativado, não é possível fazer isso. Atribua à propriedade o valor de uma variável de estado que você definirá em um momento.
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 estar ativado quando
tonight
fornull
. - O botão Stop precisa ser ativado quando
tonight
não fornull
. - O botão Clear só deverá 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
. Uma snackbar oferece um breve feedback sobre uma operação usando uma mensagem na parte inferior da tela. Um snackbar desaparece após um tempo limite, após uma interação do usuário em outro lugar na tela ou depois que o usuário desliza o snackbar para fora da tela.
A exibição da snackbar é uma tarefa da IU que precisa acontecer no fragmento. A decisão de mostrar o snackbar acontece no ViewModel
. Para configurar e acionar uma snackbar quando os dados são apagados, use a mesma técnica usada para acionar a navegação.
- No
SleepTrackerViewModel
, crie o evento encapsulado.
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- Em seguida, implemente o
doneShowingSnackbar()
.
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
- No
SleepTrackerFragment
, emonCreateView()
, adicione um observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- Dentro do bloco de observadores, exiba a snackbar e redefina imediatamente o evento.
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()
}
- No
SleepTrackerViewModel
, acione o evento no métodoonClear()
. Para fazer isso, defina o valor do evento comotrue
no blocolaunch
:
_showSnackbarEvent.value = true
- Crie e execute seu app.
Projeto do Android Studio: TrackMySleepQualityFinal
A implementação do monitoramento da qualidade do sono neste aplicativo é como tocar uma música conhecida em uma nova tecla. Embora os detalhes mudem, o padrão subjacente do que você fez nos codelabs anteriores desta lição continua o mesmo. Observar esses padrões torna a programação muito mais rápida, pois você pode reutilizar o código de aplicativos existentes. Veja alguns dos padrões usados no curso até agora:
- Crie uma
ViewModel
e umaViewModelFactory
e configure uma fonte de dados. - Acione a navegação. Para separar as preocupações, coloque o gerenciador de cliques no modelo de visualização e a navegação no fragmento.
- Use o encapsulamento com o
LiveData
para monitorar e responder a mudanças de estado. - Use transformações com
LiveData
. - Crie um banco de dados Singleton.
- Configurar corrotinas para operações de banco de dados.
Navegação como gatilho
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 os gerenciadores de
onClick
para acionar a navegação para um fragmento de destino. - Como alternativa, para ativar a navegação de um fragmento para o próximo:
- Defina um valor de
LiveData
para registrar se a navegação precisa ocorrer. - Anexe um observador a esse valor de
LiveData
. - O código então altera esse valor sempre que a navegação precisa ser acionada ou concluída.
Configurar 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 "enabled" 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
. - Os mapas de transformação podem ser usados para definir o valor do atributo
enabled
dos botões com base no estado de outro objeto ou variável.
Veja outros pontos abordados neste codelab:
- Para acionar notificações para o usuário, você pode usar a mesma técnica usada para acionar a navegação.
- Você pode usar um
Snackbar
para notificar o usuário.
Curso da Udacity:
- Como desenvolver apps Android com Kotlin (link em inglês)
Documentação do desenvolvedor Android:
Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:
- Se necessário, atribua o dever de casa.
- Informe aos alunos como enviar o dever de casa.
- Atribua nota aos trabalhos de casa.
Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.
Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.
Responda a 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 será ou não acionada.
Quais são as etapas para usar um valor de LiveData
chamado gotoBlueFragment
para acionar a navegação do fragmento vermelho para o fragmento azul? Selecione todas as opções aplicáveis.
- Em
ViewModel
, defina o valorLiveData
degotoBlueFragment
. - No
RedFragment
, observe o valorgotoBlueFragment
. Implemente o códigoobserve{}
para navegar atéBlueFragment
quando apropriado e 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 gerenciador
onClick
para oView
em que o usuário clica para navegar atéBlueFragment
, em que o gerenciadoronClick
observa o valorgoToBlueFragment
.
Pergunta 2
É possível mudar se um Button
está ativado (clicável) ou não usando LiveData
. Como garantir que o app mude o botão UpdateNumber
para que:
- O botão será ativado se
myNumber
tiver um valor maior que 5. - O botão não será 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 a NumbersViewModel
, conforme mostrado aqui:
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
Suponha que o código 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 cinco.
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
daupdate_number_button button
comoNumberViewModel.enableUpdateNumbersButton
.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
- Na
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
daupdate_number_button button
como"Observable"
.
Vá para a próxima lição:
Para ver links de outros codelabs neste curso, consulte a página de destino dos codelabs do curso Conceitos básicos do Kotlin para Android.