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
safeArgspara transmitir dados entre eles. - Ver modelos, fábricas de modelos, transformações,
LiveDatae 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
LiveDatapara 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
LiveDatapara acionar a exibição de uma snackbar. - Use
LiveDatapara 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
SleepTrackerFragmentparaSleepQualityFragmente de volta deSleepQualityFragmentparaSleepTrackerFragment.
- Inspecione o código de navigation.xml. Procure especificamente o
<argument>chamadosleepNightKey.
Quando o usuário vai deSleepTrackerFragmentparaSleepQualityFragment,, o app transmite umsleepNightKeypara oSleepQualityFragmentda 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éSleepQualityFragmentpara coletar uma classificação de qualidade. - Em
SleepTrackerViewModel, crie umLiveDataque mude quando você quiser que o app navegue até oSleepQualityFragment. Use o encapsulamento para expor apenas uma versão recuperável doLiveDataaoViewModel.
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 _navigateToSleepQualityno 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
SleepTrackerFragmentprecisa observar _navigateToSleepQualitypara 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
SleepQualityViewModelque receba umsleepNightKeye um banco de dados como argumentos. Assim como fez com oSleepTrackerViewModel, você precisa transmitir odatabaseda fábrica. Você também precisa transmitir osleepNightKeyda navegação.
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}- Na classe
SleepQualityViewModel, defina umJobe umuiScope, e substituaonCleared().
.
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}- Para voltar ao
SleepTrackerFragmentusando o mesmo padrão acima, declare_navigateToSleepTracker. ImplementenavigateToSleepTrackeredoneNavigating().
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
uiScopee mude para o dispatcher de E/S. - Receba
tonightusandosleepNightKey. - 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.kte 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 oargumentsque 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
dataSourcee osleepNightKey.
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)- Receba uma referência
ViewModel.
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)- Adicione o
ViewModelao 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:enableda 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
SleepTrackerViewModele 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
tonightfornull. - O botão Parar precisa estar ativado quando
tonightnão fornull. - O botão Limpar só deve ser ativado se
nightse, 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 comotruedentro 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
ViewModele umViewModelFactorye 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
LiveDatapara 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
onClickpara 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
LiveDatapara 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 emTextViewe herdado por todas as subclasses, incluindoButton. - O atributo
android:enableddetermina se umViewestá ativado ou não. O significado de "ativado" varia de acordo com a subclasse. Por exemplo, umEditTextnão ativado impede que o usuário edite o texto contido, e umButtonnão ativado impede que o usuário toque no botão. - O atributo
enablednão é o mesmo que o atributovisibility. - É possível usar mapas de transformação para definir o valor do atributo
enabledde 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
Snackbarpara 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 valorgotoBlueFragmentdeLiveData. - Em
RedFragment, observe o valorgotoBlueFragment. Implemente o códigoobserve{}para navegar atéBlueFragmentquando apropriado e, em seguida, redefina o valor degotoBlueFragmentpara indicar que a navegação foi concluída. - Verifique se o código define a variável
gotoBlueFragmentcomo o valor que aciona a navegação sempre que o app precisa ir deRedFragmentparaBlueFragment. - Verifique se o código define um manipulador
onClickpara oViewem que o usuário clica para navegar atéBlueFragment, em que o manipuladoronClickobserva 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
myNumbertiver um valor maior que 5. - O botão não fica ativado se
myNumberfor 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:enableddoupdate_number_button buttoncomoNumberViewModel.enableUpdateNumbersButton.
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"- No
Fragmentque usa a classeNumbersViewModel, adicione um observador ao atributoenableddo 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:enableddoupdate_number_button buttoncomo"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.