Conceitos básicos do Kotlin para Android 06.3: usar o LiveData para controlar os estados do botão

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

  1. Para começar, continue com seu próprio código do final do último codelab ou baixe o código inicial.
  2. No código inicial, inspecione o SleepQualityFragment. Essa classe aumenta o layout, recebe o aplicativo e retorna binding.root.
  3. Abra navigation.xml no editor de design. Você vê que há um caminho de navegação de SleepTrackerFragment para SleepQualityFragment e de volta de SleepQualityFragment para SleepTrackerFragment.



  4. Inspecione o código de navigation.xml. Procure especificamente o <argument> chamado sleepNightKey.

    Quando o usuário vai de SleepTrackerFragment para SleepQualityFragment,, o app transmite um sleepNightKey para o SleepQualityFragment 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.

  1. 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.
  2. Em SleepTrackerViewModel, crie um LiveData que mude quando você quiser que o app navegue até o SleepQualityFragment. Use o encapsulamento para expor apenas uma versão recuperável do LiveData ao ViewModel.

    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
  1. Adicione uma função doneNavigating() que redefine a variável que aciona a navegação.
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. 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 bloco launch{}. Essa variável está definida como night. Quando essa variável tem um valor, o app navega até o SleepQualityFragment, transmitindo o night.
    .
_navigateToSleepQuality.value = oldNight
  1. O SleepTrackerFragment precisa observar _navigateToSleepQuality para que o app saiba quando navegar. No SleepTrackerFragment, em onCreateView(), adicione um observador para navigateToSleepQuality(). A importação para isso é ambígua, e você precisa importar androidx.lifecycle.Observer.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. No bloco do observador, navegue e transmita o ID da noite atual. Em seguida, chame doneNavigating(). Se a importação for ambígua, importe androidx.navigation.fragment.findNavController.
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. 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

  1. No pacote sleepquality, crie ou abra SleepQualityViewModel.kt.
  2. Crie uma classe SleepQualityViewModel que receba um sleepNightKey e um banco de dados como argumentos. Assim como fez com o SleepTrackerViewModel, você precisa transmitir o database da fábrica. Você também precisa transmitir o sleepNightKey da navegação.
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. Na classe SleepQualityViewModel, defina um Job e um uiScope, e substitua onCleared().
    .
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Para voltar ao SleepTrackerFragment usando o mesmo padrão acima, declare _navigateToSleepTracker. Implemente navigateToSleepTracker e doneNavigating().
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. 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 usando sleepNightKey.
  • 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
        }
    }
  1. No pacote sleepquality, crie ou abra SleepQualityViewModelFactory.kt e adicione a classe SleepQualityViewModelFactory, 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

  1. Abra SleepQualityFragment.kt.
  2. Em onCreateView(), depois de receber o application, você precisa receber o arguments que veio com a navegação. Esses argumentos estão em SleepQualityFragmentArgs. É necessário extraí-los do pacote.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Em seguida, gere o dataSource.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Crie uma fábrica, transmitindo o dataSource e o sleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Receba uma referência ViewModel.
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. 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
  1. 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

  1. Abra o arquivo de layout fragment_sleep_quality.xml. No bloco <data>, adicione uma variável para o SleepQualityViewModel.
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. 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)}"
  1. 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.

  1. Abra o arquivo de layout fragment_sleep_tracker.xml.
  2. Adicione a propriedade android:enabled a cada botão. A propriedade android: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}"
  1. 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 for null.
  • O botão Parar precisa estar ativado quando tonight não for null.
  • 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()
}
  1. 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.

  1. No SleepTrackerViewModel, crie o evento encapsulado.
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. Em seguida, implemente doneShowingSnackbar().
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. No SleepTrackerFragment, em onCreateView(), adicione um observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. 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()
   }
  1. Em SleepTrackerViewModel, acione o evento no método onClear(). Para isso, defina o valor do evento como true dentro do bloco launch:
_showSnackbarEvent.value = true
  1. 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 um ViewModelFactory 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 em TextView e herdado por todas as subclasses, incluindo Button.
  • O atributo android:enabled determina se um View está ativado ou não. O significado de "ativado" varia de acordo com a subclasse. Por exemplo, um EditText não ativado impede que o usuário edite o texto contido, e um Button não ativado impede que o usuário toque no botão.
  • O atributo enabled não é o mesmo que o atributo visibility.
  • É 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 valor gotoBlueFragment de LiveData.
  • Em RedFragment, observe o valor gotoBlueFragment. Implemente o código observe{} para navegar até BlueFragment quando apropriado e, em seguida, redefina o valor de gotoBlueFragment 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 de RedFragment para BlueFragment.
  • Verifique se o código define um manipulador onClick para o View em que o usuário clica para navegar até BlueFragment, em que o manipulador onClick observa o valor goToBlueFragment.

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ável LiveData, myNumber, que representa o número. Defina também uma variável cujo valor é definido chamando Transform.map() na variável myNumber, que retorna um booleano indicando se o número é maior que 5 ou não.

    Especificamente, em ViewModel, adicione o seguinte código:
val myNumber: LiveData<Int>

val enableUpdateNumberButton = Transformations.map(myNumber) {
   myNumber > 5
}
  • No layout XML, defina o atributo android:enabled do update_number_button button como NumberViewModel.enableUpdateNumbersButton.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • No Fragment que usa a classe NumbersViewModel, adicione um observador ao atributo enabled do botão.

    Especificamente, em Fragment, 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 do update_number_button button como "Observable".

Comece a próxima lição: 7.1 Conceitos básicos do RecyclerView

Para acessar links de outros codelabs neste curso, consulte a página inicial dos codelabs de conceitos básicos do Kotlin para Android.