Introdução

Este tutorial recapitula como usar ViewModel e fragmentos juntos para implementar a navegação. Lembre-se que o objetivo é colocar a lógica de quando navegar no ViewModel, mas definir os caminhos nos fragmentos e no arquivo de navegação. Para atingir esse objetivo, você usa modelos de vista, fragmentos, LiveData e observadores.

O tutorial conclui mostrando uma maneira inteligente de rastrear estados de botão com código mínimo, de modo que cada botão seja habilitado e clicável apenas quando faz sentido para o usuário tocar nele.

O que você já deveria saber

Você deve estar familiarizado com:

O que aprenderá

O que fará

Neste tutorial, você constrói a gravação da qualidade do sono e a IU finalizada do aplicativo TrackMySleepQuality.

O aplicativo possui duas telas, representadas por fragmentos, conforme mostrado na figura abaixo.

A primeira tela, mostrada à esquerda, possui botões para iniciar e parar o rastreamento. A tela mostra todos os dados de sono do usuário. O botão Clear exclui permanentemente todos os dados que o aplicativo coletou para o usuário.

A segunda tela, mostrada à direita, é para selecionar uma classificação de qualidade do sono. No aplicativo, a classificação é representada numericamente. Para fins de desenvolvimento, o aplicativo mostra os ícones de rosto e seus equivalentes numéricos.

O fluxo do usuário é o seguinte:

Este aplicativo usa uma arquitetura simplificada, conforme mostrado abaixo no contexto da arquitetura completa. O aplicativo usa apenas os seguintes componentes:

Este tutorial pressupõe que você saiba como implementar a navegação usando fragmentos e o arquivo de navegação. Para economizar seu trabalho, uma boa parte desse código é fornecida.

Etapa 1: Inspecione o código

  1. Para começar, continue com seu próprio código do final do último tutorial ou baixe o código inicial.
  2. Em seu código inicial, inspecione o SleepQualityFragment. Essa classe infla o layout, obtém 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 novamente de SleepQualityFragment para SleepTrackerFragment.



  4. Inspecione o código para navigation.xml. Em particular, procure o <argument> denominado sleepNightKey.

    Quando o usuário vai de SleepTrackerFragment para SleepQualityFragment, o aplicativo passará uma sleepNightKey para o SleepQualityFragment para a noite que precisa ser atualizada.

Etapa 2: Adicione navegação para rastreamento da qualidade do sono

O grafo de navegação já inclui os caminhos do SleepTrackerFragment para o SleepQualityFragment e vice-versa. No entanto, os tratadores de cliques que implementam a navegação de um fragmento para o próximo ainda não estão codificados. Você adiciona esse código agora no ViewModel.

No tratador de cliques, você define um LiveData que muda quando você deseja que o aplicativo navegue para um destino diferente. O fragmento observa este LiveData. Quando os dados são alterados, o fragmento navega até o destino e informa ao modelo de vistas que está pronto, 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 Stop, o aplicativo navegue até SleepQualityFragment para coletar uma classificação de qualidade.
  2. Em SleepTrackerViewModel, crie um LiveData que muda quando você deseja que o aplicativo navegue até SleepQualityFragment. Use o encapsulamento para expor apenas uma versão gettable do LiveData para o ViewModel.

    Você pode colocar este 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 dispara a navegação.
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. No tratador 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 último método dentro do bloco launch{}. Observe que esta variável é definida para night. Quando essa variável tem um valor, o aplicativo navega até o SleepQualityFragment, passando a noite.
_navigateToSleepQuality.value = oldNight
  1. O SleepTrackerFragment precisa observar _navigateToSleepQuality para que o aplicativo saiba quando navegar. No SleepTrackerFragment, em onCreateView(), adicione um observador para navigateToSleepQuality(). Observe que a importação para isso é ambígua e você precisa importar androidx.lifecycle.Observer.
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. Dentro do bloco do observador, navegue e passe o ID da noite atual e 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. Construa e execute seu aplicativo. Toque em Start e, em seguida, toque em Stop, que o levará para a tela SleepQualityFragment. Para voltar, use o botão Voltar do sistema.

Nesta tarefa, você registra a qualidade do sono e navega de volta ao fragmento do rasthreador de sono. O display deve ser atualizado automaticamente para mostrar o valor atualizado ao usuário. Você precisa criar um ViewModel e uma ViewModelFactory, e você precisa atualizar o SleepQualityFragment.

Etapa 1: Crie um ViewModel e uma ViewModelFactory

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

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Para navegar de volta 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 tratador de um clique, onSetSleepQuality(), para todas as imagens com qualidade de sono usar.

    Use o mesmo padrão de corrotina do tutorial anterior:

Observe que o exemplo de código abaixo faz todo o trabalho no tratador de cliques, em vez de fatorar a operação do banco de dados no contexto diferente.

fun onSetSleepQuality(quality: Int) {
        uiScope.launch {
            withContext(Dispatchers.IO) {
                val tonight = database.get(sleepNightKey) ?: return@withContext
                tonight.sleepQuality = quality
                database.update(tonight)
            }

            _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 clichê que você viu antes. Inspecione o código antes de prosseguir.
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: Atualize o SleepQualityFragment

  1. Abra SleepQualityFragment.kt.
  2. Em onCreateView(), depois de obter o application, você precisa obter os arguments que vieram com a navegação. Esses argumentos estão em SleepQualityFragmentArgs. Você precisa extraí-los do pacote.
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. Em seguida, obtenha a dataSource.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Crie uma fábrica, passando a dataSource e a sleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. Obtenha uma referência de ViewModel.
val sleepQualityViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. Adicione o 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) {
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

Etapa 3: Atualize o arquivo de layout e execute o aplicativo

  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 com qualidade de sono, adicione um tratador de cliques como o mostrado abaixo. Combine a classificação de qualidade com a imagem.
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. Limpe e reconstrua seu projeto. Isso deve resolver quaisquer erros com o objeto de vinculação. Caso contrário, limpe o cache (File > Invalidate Caches / Restart) e reconstrua seu aplicativo.

Parabéns! Você acabou de construir um aplicativo de banco de dados Room completo usando corrotinas.

Agora seu aplicativo funciona muito bem. O usuário pode tocar em Start e Stop quantas vezes quiser. Quando o usuário toca em Stop, ele pode inserir uma qualidade de sono. Quando o usuário toca em Clear, todos os dados são limpos silenciosamente em segundo plano. No entanto, todos os botões estão sempre habilitados e clicáveis, o que não interrompe o aplicativo, mas 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 para que os usuários possam fazer apenas a escolha certa. Você pode usar um método semelhante para exibir uma mensagem amigável após todos os dados terem sido apagados.

Etapa 1: Atualize estados do botão

A ideia é definir o estado do botão para que, no início, apenas o botão Start esteja habilitado, o que significa que é clicável.

Depois que o usuário toca em Start, o botão Stop é habilitado e Start não. O botão Clear somente é habilitado 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á habilitado ou não. (Um botão habilitado pode ser tocado; um botão desabilitado não). Provenha à 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 testa.
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. Execute seu aplicativo e experimente os botões.

Etapa 2: Use uma snackbar para notificar o usuário

Depois que o usuário limpar o banco de dados, mostre ao usuário uma confirmação usando o widget Snackbar. Uma snackbar fornece um breve retorno sobre uma operação por meio de uma mensagem na parte inferior da tela. Uma 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 a snackbar para fora da tela.

Mostrando a snackbar é uma tarefa de IU e deve acontecer no fragmento. A decisão de mostrar a snackbar acontece no ViewModel. Para configurar e acionar uma snackbar quando os dados forem apagados, você pode usar 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 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, exiba a snackbar e reinicie imediatamente o evento.
   if (it == true) {
       Snackbar.make(
               activity!!.findViewById(android.R.id.content),
               getString(R.string.cleared_message),
               Snackbar.LENGTH_SHORT
       ).show()
       sleepTrackerViewModel.doneShowingSnackbar()
   }
  1. Em SleepTrackerViewModel, acione o evento no método onClear(). Para fazer isso, defina o valor do evento como true dentro do bloco launch:
_showSnackbarEvent.value = true
  1. Construa e execute seu aplicativo!

Projeto Android Studio: TrackMySleepQualityFinal

Implementando o rastreamento da qualidade do sono neste aplicativo é como tocar uma peça familiar de música em um novo tom. Embora os detalhes mudem, o padrão subjacente do que você fez nos tutoriais anteriores desta lição permanece o mesmo. Estando ciente desses padrões torna a codificação muito mais rápida, pois você pode reutilizar o código de aplicativos existentes. Aqui estão alguns dos padrões usados ​​neste curso até agora:

Navegação desencadeada

Você define caminhos de navegação possíveis entre fragmentos em um arquivo de navegação. Existem algumas maneiras diferentes de acionar a navegação de um fragmento para o próximo. Esses incluem:

Configurando o android: Atributo ativado

Outros pontos abordados neste tutorial:

Documentação para desenvolvimento em Android:

Esta seção lista as possíveis tarefas de casa para os alunos que estão trabalhando neste tutorial como parte de um curso ministrado por um instrutor.

Responda a essas perguntas

Pergunta 1

Uma maneira de habilitar seu aplicativo para acionar a navegação de um fragmento para o próximo é usar um valor LiveData para indicar se deve ou não acionar a navegação.

Quais são as etapas para usar um valor LiveData, chamado gotoBlueFragment, para acionar a navegação do fragmento vermelho para o fragmento azul? Selecione tudo que se aplica:

Pergunta 2

Você pode alterar se um Button está habilitado (clicável) ou não usando LiveData. Como você garantiria que seu aplicativo mudasse o botão UpdateNumber para que:

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 tudo que se aplica.

val myNumber: LiveData<Int>

val enableUpdateNumberButton = Transformations.map(myNumber) {
   myNumber > 5
}
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
   myNumber > 5
})

Comece para a próxima lição: 07.1: Fundamentos do RecyclerView

Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.