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

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

  1. Para começar, continue com seu próprio código do fim do último codelab ou faça o download do código inicial.
  2. No código inicial, inspecione o SleepQualityFragment. Essa classe infla o layout, recebe o app e retorna binding.root.
  3. Abra o arquivo navigation.xml no editor de design. Há um caminho de navegação do SleepTrackerFragment até o SleepQualityFragment, e vice-versa.



    .
  4. Inspecione o código do navigation.xml. Procure pelo <argument> chamado sleepNightKey.

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

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

    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
  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 para o SleepQualityFragment. Defina a variável _navigateToSleepQuality no final da função como a última coisa dentro do bloco launch{}. Essa variável é definida como night. Quando essa variável tiver um valor, o app navegará para o SleepQualityFragment, transmitindo durante a noite.
_navigateToSleepQuality.value = oldNight
  1. O SleepTrackerFragment precisa observar _navigateToSleepQuality para que o app saiba quando navegar. No SleepTrackerFragment, em onCreateView(), adicione um observador para a navigateToSleepQuality(). A importação para isso é ambígua, e você precisa importar androidx.lifecycle.Observer.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. No bloco de observadores, navegue e transmita o ID da noite atual e, em seguida, chame doneNavigating(). Se sua 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 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

  1. No pacote sleepquality, crie ou abra SleepQualityViewModel.kt.
  2. Crie uma classe SleepQualityViewModel que recebe um sleepNightKey e um banco de dados como argumentos. Assim como foi feito no 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 modifique 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 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 o sleepNightKey.
  • 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
        }
    }
  1. No pacote sleepquality, crie ou abra SleepQualityViewModelFactory.kt e adicione a classe SleepQualityViewModelFactory, 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

  1. Abra o SleepQualityFragment.kt
  2. Em onCreateView(), depois de receber o application, você precisará do arguments que veio com a navegação. Esses argumentos estão em SleepQualityFragmentArgs. Você precisa extraí-los do pacote.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Em seguida, acesse o dataSource.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Crie uma fábrica, transmitindo o dataSource e a sleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Receber uma referência ViewModel.
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. 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
  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 a SleepQualityViewModel.
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. 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)}"
  1. 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.

  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. É 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}"
  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 estar ativado quando tonight for null.
  • O botão Stop precisa ser ativado quando tonight não for null.
  • 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()
}
  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. 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.

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

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. Em seguida, implemente o doneShowingSnackbar().
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. No SleepTrackerFragment, em onCreateView(), adicione um observador:
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. 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()
   }
  1. No SleepTrackerViewModel, acione o evento no método onClear(). Para fazer isso, defina o valor do evento como true no bloco launch:
_showSnackbarEvent.value = true
  1. 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 uma ViewModelFactory 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 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 "enabled" 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.
  • 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:

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 valor LiveData de gotoBlueFragment.
  • No RedFragment, observe o valor gotoBlueFragment. Implemente o código observe{} para navegar até BlueFragment quando apropriado e 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 gerenciador onClick para o View em que o usuário clica para navegar até BlueFragment, em que o gerenciador onClick observa o valor goToBlueFragment.

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á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 cinco.

    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 da update_number_button button como NumberViewModel.enableUpdateNumbersButton.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
  • Na 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 da update_number_button button como "Observable".

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

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.