Introdução

No tutorial anterior, você atualizou o aplicativo TrackMySleepQuality para exibir dados sobre a qualidade do sono em um RecyclerView. As técnicas que você aprendeu ao construir seu primeiro RecyclerView são suficientes para a maioria dos RecyclerViews que exibem listas simples que não são muito grandes. No entanto, existem várias técnicas que tornam o RecyclerView mais eficiente para listas grandes e tornam seu código mais fácil de manter e estender para listas e grades complexas.

Neste tutorial, você desenvolve o aplicativo sleep-tracker do tutorial anterior. Você aprende uma maneira mais eficaz de atualizar a lista de dados do sono e como usar a vinculação de dados com RecyclerView. (Se você não tem o aplicativo do tutorial anterior, pode baixar o código inicial para este tutorial).

O que você já deveria saber

O que aprenderá

O que fará

O aplicativo sleep-tracker 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 alguns dos 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.

Este aplicativo foi arquitetado para usar um controlador de IU, ViewModel e LiveData, e um banco de dados Room para persistir os dados de sono.

Os dados do sono são exibidos em um RecyclerView. Neste tutorial, você constrói o DiffUtil e a parte de vinculação de dados para o RecyclerView. Após este tutorial, seu aplicativo terá exatamente a mesma aparência, mas será mais eficiente e mais fácil de escalar e manter.

Você pode continuar usando o aplicativo SleepTracker do tutorial anterior ou pode baixar o aplicativo RecyclerViewDiffUtilDataBinding-Starter do GitHub.

  1. Se necessário, baixe o aplicativo RecyclerViewDiffUtilDataBinding-Starter do GitHub e abra o projeto no Android Studio.
  2. Execute o aplicativo.
  3. Abra o arquivo SleepNightAdapter.kt.
  4. Inspecione o código para se familiarizar com a estrutura do aplicativo. Consulte o diagrama abaixo para uma recapitulação do uso do RecyclerView com o padrão do adaptador para exibir os dados de sono para o usuário.

O método notificationDataSetChanged() é ineficiente

Para informar ao RecyclerView que um item da lista foi alterado e precisa ser atualizado, o código atual chama notifyDataSetChanged() no SleepNightAdapter, conforme mostrado abaixo.

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

No entanto, notifyDataSetChanged() diz ao RecyclerView que a lista inteira é potencialmente inválida. Como resultado, o RecyclerView vincula e redesenha todos os itens da lista, incluindo itens que não são visíveis na tela. É muito trabalho desnecessário. Para listas grandes ou complexas, esse processo pode demorar o suficiente para que a tela pisque ou gagueje conforme o usuário rola a lista.

Para corrigir esse problema, você pode dizer ao RecyclerView exatamente o que mudou. RecyclerView pode então atualizar apenas as vistas que mudaram na tela.

RecyclerView tem uma API rica para atualizar um único elemento. Você pode usar notifyItemChanged() para informar ao RecyclerView que um item foi alterado, e você pode usar funções semelhantes para itens que são adicionados, removidos, ou movido. Você poderia fazer tudo manualmente, mas essa tarefa não seria trivial e envolveria um pouco de código.

Felizmente, existe uma maneira melhor.

O DiffUtil é eficiente e faz o trabalho difícil

RecyclerView tem uma classe chamada DiffUtil que é para calcular as diferenças entre duas listas. DiffUtil pega uma lista antiga e uma nova lista e descobre o que é diferente. Ele encontra itens que foram adicionados, removidos ou alterados. Em seguida, ele usa um algoritmo chamado algoritmo de diferença de Eugene W. Myers para descobrir o número mínimo de alterações a serem feitas na lista antiga para produzir a nova lista.

Depois que DiffUtil descobre o que mudou, RecyclerView pode usar essas informações para atualizar apenas os itens que foram alterados, adicionados, removidos ou movidos, o que é muito mais eficiente do que refazer a lista inteira.

Nesta tarefa, você atualiza o SleepNightAdapter para usar o DiffUtil para otimizar o RecyclerView para alterações nos dados.

Etapa 1: Implemente SleepNightDiffCallback

Para usar a funcionalidade da classe DiffUtil, estenda DiffUtil.ItemCallback.

  1. Abra SleepNightAdapter.kt.
  2. Abaixo da definição de classe completa para SleepNightAdapter, crie uma classe de nível superior chamada SleepNightDiffCallback que estende DiffUtil.ItemCallback. Passe SleepNight como um parâmetro genérico.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. Coloque o cursor no nome da classe SleepNightDiffCallback.
  2. Pressione Alt+Enter(Option+Enter no Mac) e selecione Implement Members.
  3. Na caixa de diálogo que é aberta, clique com a tecla shift pressionada para selecionar os métodos areItemsTheSame() e areContentsTheSame() e clique em OK.

    Isso gera stubs dentro de SleepNightDiffCallback para os dois métodos, conforme mostrado abaixo. DiffUtil usa esses dois métodos para descobrir como a lista e os itens foram alterados.
    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. Dentro de areItemsTheSame(), substitua o TODO pelo código que testa se os dois itens SleepNight passados, oldItem e newItem são iguais. Se os itens tiverem o mesmo nightId, eles são o mesmo item, então retorne true. Caso contrário, retorne false. DiffUtil usa este teste para ajudar a descobrir se um item foi adicionado, removido ou movido.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. Dentro de areContentsTheSame(), verifique se oldItem e newItem contêm os mesmos dados; isto é, se eles são iguais. Esta verificação de igualdade verificará todos os campos, pois SleepNight é uma classe de dados. As classes Data definem automaticamente equals e alguns outros métodos. Se houver diferenças entre oldItem e newItem, este código informa a DiffUtil que o item foi atualizado.
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

É um padrão comum usar um RecyclerView para exibir uma lista que muda. RecyclerView fornece uma classe de adaptador, ListAdapter, que ajuda a construir um adaptador RecyclerView apoiado por uma lista.

ListAdapter mantém o controle da lista e notifica o adaptador quando a lista é atualizada.

Etapa 1: Mude seu adaptador para estender ListAdapter

  1. No arquivo SleepNightAdapter.kt, altere a assinatura da classe de SleepNightAdapter para estender o ListAdapter.
  2. Se solicitado, importe androidx.recyclerview.widget.ListAdapter.
  3. Adicione SleepNight como o primeiro argumento para o ListAdapter, antes de SleepNightAdapter.ViewHolder.
  4. Adicione SleepNightDiffCallback() como um parâmetro para o construtor. O ListAdapter usará isso para descobrir o que mudou na lista. Sua assinatura de classe SleepNightAdapter finalizada deve ser semelhante à mostrada abaixo.
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. Dentro da classe SleepNightAdapter, exclua o campo data, incluindo o configurador. Você não precisa mais dele, pois ListAdapter mantém o controle da lista.
  2. Exclua a substituição de getItemCount(), pois o ListAdapter implementa esse método.
  3. Para se livrar do erro em onBindViewHolder(), altere a variável item. Em vez de usar data para obter um item, chame o método getItem(position) que o ListAdapter fornece.
val item = getItem(position)

Etapa 2: Use submitList() para manter a lista atualizada

Seu código precisa informar ao ListAdapter quando uma lista alterada está disponível. ListAdapter fornece um método chamado submitList() para informar ao ListAdapter que uma nova versão da lista está disponível. Quando este método é chamado, o ListAdapter difere a nova lista da antiga e detecta itens que foram adicionados, removidos, movidos ou alterados. Em seguida, o ListAdapter atualiza os itens mostrados por RecyclerView.

  1. Abra SleepTrackerFragment.kt.
  2. Em onCreateView(), no observador em sleepTrackerViewModel, encontre o erro onde a variável data que você excluiu é referenciada.
  3. Substitua adapter.data = it por uma chamada para adapter.submitList(it). O código atualizado é mostrado abaixo.

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. Execute seu aplicativo. Ele roda mais rápido, talvez não perceptível se sua lista for pequena.

Nesta tarefa, você usa a mesma técnica dos tutoriais anteriores para configurar a vinculação de dados e elimina chamadas para findViewById().

Etapa 1: Adicione vinculação de dados ao arquivo de layout

  1. Abra o arquivo de layout list_item_sleep_night.xml na guia Text.
  2. Coloque o cursor na etiqueta ConstraintLayout e pressione Alt+Enter (Option+Enter em um Mac). O menu de intenção (o menu "solução rápida") é aberto.
  3. Selecione Convert to data binding layout. Isso envolve o layout em <layout> e adiciona uma etiqueta <data> dentro.
  4. Role de volta para o topo, se necessário, e dentro da etiqueta <data>, declare uma variável chamada sleep.
  5. Faça do seu type o nome qualificado de SleepNight, com.example.android.trackmysleepquality.database.SleepNight. Sua etiqueta <data> finalizada deve se parecer com a mostrada abaixo.
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. Para forçar a criação do objeto Binding, selecione Build > Clean Project e, em seguida, selecione Build > Rebuild Project. (Se você ainda tiver problemas, selecione File > Invalidate Caches / Restart). O objeto de vinculação ListItemSleepNightBinding, junto com o código relacionado, é adicionado aos arquivos gerados do projeto.

Etapa 2: Infle o layout do item usando vinculação de dados

  1. Abra SleepNightAdapter.kt.
  2. Na classe ViewHolder, encontre o método from().
  3. Exclua a declaração da variável view.

Código para delete:

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. Onde a variável view estava, defina uma nova variável chamada binding que infla o objeto de vinculação ListItemSleepNightBinding, conforme mostrado abaixo. Faça a importação necessária do objeto de vinculação.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. No final da função, em vez de retornar a view, retorne binding.
return ViewHolder(binding)
  1. Para se livrar do erro, coloque o cursor na palavra binding. Pressione Alt+Enter (Option+Enter em um Mac) para abrir o menu de intenção.
  1. Selecione Change parameter 'itemView' type of primary constructor of class 'ViewHolder' to 'ListItemSleepNightBinding'. Isso atualiza o tipo de parâmetro da classe ViewHolder.

  1. Role para cima até a definição de classe do ViewHolder para ver a mudança na assinatura. Você vê um erro para itemView, pois alterou itemView para binding no método from().

    Na definição da classe ViewHolder, clique com o botão direito em uma das ocorrências de itemView e selecione Refactor> Rename. Mude o nome para binding.
  2. Prefixe o parâmetro do construtor binding com val para torná-lo uma propriedade.
  3. Na chamada para a classe pai, RecyclerView.ViewHolder, altere o parâmetro de binding para binding.root. Você precisa passar uma View, e binding.root é a raiz ConstraintLayout em seu layout de item.
  4. Sua declaração de classe concluída deve ser semelhante ao código abaixo.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

Você também vê um erro nas chamadas para findViewById() e corrige isso a seguir.

Etapa 3: Substitua findViewById()

Agora você pode atualizar as propriedades sleepLength, quality e qualityImage para usar o objeto binding em vez de findViewById().

  1. Altere as inicializações de sleepLength, qualityString e qualityImage para usar as vistas do objeto binding, conforme mostrado abaixo. Depois disso, seu código não deve mostrar mais erros.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

Com o objeto de vinculação no lugar, você não precisa mais definir as propriedades sleepLength, quality e qualityImage. DataBinding armazenará em cache as pesquisas, portanto, não há necessidade de declarar essas propriedades.

  1. Clique com o botão direito nos nomes das propriedades sleepLength, quality e qualityImage. Selecione Refactor > Inline ou pressione Control+Command+N (Option+Command+N em um Mac).
  2. Execute seu aplicativo. (Você pode precisar Clean e Rebuild seu projeto se ele tiver erros).

Nesta tarefa, você atualiza seu aplicativo para usar vinculação de dados com adaptadores de vinculação para definir os dados em suas vistas.

Em um tutorial anterior, você usou a classe Transformations para pegar o LiveData e gerar strings formatadas para exibir em vistas de texto. No entanto, se você precisar vincular diferentes tipos ou tipos complexos, poderá fornecer adaptadores de vinculação para ajudar a vinculação de dados a usar esses tipos. Adaptadores de vinculação são adaptadores que pegam seus dados e os adaptam em algo que a vinculação de dados pode usar para vincular uma vista, como texto ou imagem.

Você implementará três adaptadores de vinculação, um para a imagem de qualidade e um para cada campo de texto. Em resumo, para declarar um adaptador de vinculação, você define um método que pega um item e uma vista e anota-o com @BindingAdapter. No corpo do método, você implementa a transformação. No Kotlin, você pode escrever um adaptador de vinculação como uma função de extensão na classe de vistas que recebe os dados.

Etapa 1: Crie adaptadores de vinculação

Observe que você terá que importar várias classes na etapa e não será chamado individualmente.

  1. Abra SleepNightAdapater.kt.
  2. Dentro da classe ViewHolder, encontre o método bind() e lembre-se do que esse método faz. Você pegará o código que calcula os valores de binding.sleepLength, binding.quality e binding.qualityImage e o usará dentro do adaptador em vez de. (Por enquanto, deixe o código como está; mova-o em uma etapa posterior).
  3. No pacote sleeptracker, crie e abra um arquivo chamado BindingUtils.kt.
  4. Declare uma função de extensão em TextView, chamada setSleepDurationFormatted, e transmita um SleepNight. Esta função será o seu adaptador para calcular e formatar a duração do sono.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. No corpo de setSleepDurationFormatted, vincule os dados à vista como você fez em ViewHolder.bind(). Chame convertDurationToFormatted() e, a seguir, defina o text de TextView como o texto formatado. (Como esta é uma função de extensão em TextView, você pode acessar diretamente a propriedade text).
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. Para informar a vinculação de dados sobre este adaptador de vinculação, anote a função com @BindingAdapter.
  2. Esta função é o adaptador para o atributo sleepDurationFormatted, então passe sleepDurationFormatted como um argumento para @BindingAdapter.
@BindingAdapter("sleepDurationFormatted")
  1. O segundo adaptador define a qualidade do sono com base no valor em um objeto SleepNight. Crie uma função de extensão chamada setSleepQualityString() em TextView e passe um SleepNight.
  2. No corpo, vincule os dados à vista como você fez em ViewHolder.bind(). Chame convertNumericQualityToString e defina o text.
  3. Anote a função com @BindingAdapter("sleepQualityString").
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. O terceiro adaptador de vinculação define a imagem em uma vista de imagem. Crie a função de extensão em ImageView, chame setSleepImage e use o código de ViewHolder.bind(), conforme mostrado abaixo.
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

Etapa 2: Atualize o SleepNightAdapter

  1. Abra SleepNightAdapter.kt.
  2. Exclua tudo no método bind(), pois agora você pode usar a vinculação de dados e seus novos adaptadores para fazer este trabalho.
fun bind(item: SleepNight) {
}
  1. Dentro de bind(), atribua sleep para item, pois você precisa informar ao objeto de vinculação sobre seu novo SleepNight.
binding.sleep = item
  1. Abaixo dessa linha, adicione binding.executePendingBindings(). Essa chamada é uma otimização que solicita que a vinculação de dados execute imediatamente qualquer vinculação pendente. É sempre uma boa ideia chamar executePendingBindings() quando você usa adaptadores de vinculação em um RecyclerView, pois pode acelerar um pouco o dimensionamento das vistas.
 binding.executePendingBindings()

Etapa 3: Adicione vinculações ao layout XML

  1. Abra list_item_sleep_night.xml.
  2. Em ImageView, adicione uma propriedade app com o mesmo nome do adaptador de vinculação que define a imagem. Passe a variável sleep, conforme mostrado abaixo.

    Esta propriedade cria a conexão entre a vista e o objeto de vinculação, através do adaptador. Sempre que sleepImage é referenciado, o adaptador adaptará os dados de SleepNight.
app:sleepImage="@{sleep}"
  1. Faça o mesmo para as vistas de texto sleep_length e quality_string. Sempre que sleepDurationFormatted ou sleepQualityString for referenciado, os adaptadores adaptarão os dados de SleepNight.
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. Execute seu aplicativo. Funciona exatamente da mesma forma que antes. Os adaptadores de vinculação cuidam de todo o trabalho de formatação e atualização das vistas conforme os dados mudam, simplificando o ViewHolder e dando ao código uma estrutura muito melhor do que antes.

Você exibiu a mesma lista nos últimos exercícios. Isso é intencional, para mostrar que a interface do Adapter permite arquitetar seu código de muitas maneiras diferentes. Quanto mais complexo for o código, mais importante será arquitetá-lo bem. Em aplicativos de produção, esses padrões e outros são usados ​​com RecyclerView. Todos os padrões funcionam e cada um tem seus benefícios. Qual você escolhe depende do que você está construindo.

Parabéns! Neste ponto, você está no caminho certo para dominar o RecyclerView no Android.

Projeto Android Studio: RecyclerViewDiffUtilDataBinding.

DiffUtil:

ListAdapter:

Vinculação de dados:

Adaptadores de vinculação:

@BindingAdapter("sleepDurationFormatted")
.app:sleepDurationFormatted="@{sleep}"

Documentação para desenvolvimento em Android:

Outros recursos:

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

Quais das seguintes opções são necessárias para usar o DiffUtil? Selecione tudo que se aplica.

▢ Estenda a classe ItemCallBack.

▢ Substitua areItemsTheSame().

▢ Substitua areContentsTheSame().

▢ Use vinculação de dados para rastrear as diferenças entre os itens.

Pergunta 2

Quais das seguintes afirmações são verdadeiras sobre adaptadores de vinculação?

▢ Um adaptador de vinculação é uma função anotada com @BindingAdapter.

▢ Usando um adaptador de vinculação permite separar a formatação de dados do recipiente de vistas.

▢ Você deve usar um RecyclerViewAdapter se quiser usar adaptadores de vinculação.

▢ Adaptadores de vinculação são uma boa solução quando você precisa transformar dados complexos.

Pergunta 3

Quando você deve considerar o uso de Transformations em vez de um adaptador de vinculação? Selecione tudo que se aplica.

▢ Seus dados são simples.

▢ Você está formatando uma string.

▢ Sua lista é muito longa.

▢ Seu ViewHolder contém apenas uma vista.

Comece a próxima lição: 07.3: GridLayout com RecyclerView