Conceitos básicos do Kotlin para Android 07.5: cabeçalhos na RecyclerView

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

Neste codelab, você vai aprender a adicionar um cabeçalho que abrange a largura da lista mostrada em um RecyclerView. Você vai criar com base no app sleep-tracker dos codelabs anteriores.

O que você já precisa saber

  • Como criar uma interface básica usando uma atividade, fragmentos e visualizações.
  • Como 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 DAO e definir entidades.
  • Como usar corrotinas para interações com bancos de dados e outras tarefas de longa duração.
  • Como implementar um RecyclerView básico com um Adapter, ViewHolder e um layout de item.
  • Como implementar a vinculação de dados para RecyclerView.
  • Como criar e usar adaptadores de vinculação para transformar dados.
  • Como usar o GridLayoutManager.
  • Como capturar e processar cliques em itens em um RecyclerView.

O que você vai aprender

  • Como usar mais de uma ViewHolder com uma RecyclerView para adicionar itens com um layout diferente. Especificamente, como usar um segundo ViewHolder para adicionar um cabeçalho acima dos itens mostrados em RecyclerView.

Atividades deste laboratório

  • Crie com base no app TrackMySleepQuality do codelab anterior desta série.
  • Adicione um cabeçalho que abranja a largura da tela acima das noites de sono mostradas no RecyclerView.

O app de monitoramento do sono com que você começa tem três telas, representadas por fragmentos, conforme mostrado na figura abaixo.

A primeira tela, mostrada à esquerda, tem botões para iniciar e interromper o rastreamento. A tela mostra alguns dos 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 no meio, é para selecionar uma classificação de qualidade do sono. A terceira tela é uma visualização de detalhes que é aberta quando o usuário toca em um item na grade.

Esse app usa uma arquitetura simplificada com um controlador de interface, um modelo de visualização e LiveData, além de um banco de dados Room para manter os dados de sono.

Neste codelab, você vai adicionar um cabeçalho à grade de itens mostrados. A tela principal final vai ficar assim:

Este codelab ensina o princípio geral de incluir itens que usam layouts diferentes em um RecyclerView. Um exemplo comum é ter cabeçalhos na sua lista ou grade. Uma lista pode ter um único cabeçalho para descrever o conteúdo do item. Uma lista também pode ter vários cabeçalhos para agrupar e separar itens em uma única lista.

O RecyclerView não sabe nada sobre seus dados ou que tipo de layout cada item tem. O LayoutManager organiza os itens na tela, mas o adaptador adapta os dados a serem mostrados e transmite os fixadores de visualização ao RecyclerView. Portanto, você vai adicionar o código para criar cabeçalhos no adaptador.

Duas maneiras de adicionar cabeçalhos

Em RecyclerView, cada item da lista corresponde a um número de índice que começa em 0. Exemplo:

[Dados reais] -> [Visualizações do adaptador]

[0: SleepNight] -> [0: SleepNight]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

Uma maneira de adicionar cabeçalhos a uma lista é modificar o adaptador para usar um ViewHolder diferente, verificando os índices em que o cabeçalho precisa ser mostrado. O Adapter será responsável por acompanhar o cabeçalho. Por exemplo, para mostrar um cabeçalho na parte de cima da tabela, é necessário retornar um ViewHolder diferente para o cabeçalho ao organizar o item indexado como zero. Todos os outros itens seriam mapeados com o deslocamento do cabeçalho, como mostrado abaixo.

[Dados reais] -> [Visualizações do adaptador]

[0: Header]

[0: SleepNight] -> [1: SleepNight]

[1: SleepNight] -> [2: SleepNight]

[2: SleepNight] -> [3: SleepNight.

Outra maneira de adicionar cabeçalhos é modificar o conjunto de dados de suporte da grade de dados. Como todos os dados que precisam ser mostrados estão armazenados em uma lista, é possível modificá-la para incluir itens que representem um cabeçalho. Isso é um pouco mais simples de entender, mas exige que você pense em como projeta seus objetos para combinar os diferentes tipos de itens em uma única lista. Implementado dessa forma, o adaptador vai mostrar os itens transmitidos a ele. Assim, o item na posição 0 é um cabeçalho, e o item na posição 1 é um SleepNight, que é mapeado diretamente para o que está na tela.

[Dados reais] -> [Visualizações do adaptador]

[0: Header] -> [0: Header]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

[3: SleepNight] -> [3: SleepNight]

Cada metodologia tem benefícios e desvantagens. Mudar o conjunto de dados não altera muito o restante do código do adaptador, e você pode adicionar lógica de cabeçalho manipulando a lista de dados. Por outro lado, usar um ViewHolder diferente verificando os índices dos cabeçalhos dá mais liberdade no layout do cabeçalho. Ele também permite que o adaptador processe como os dados são adaptados à visualização sem modificar os dados de suporte.

Neste codelab, você vai atualizar seu RecyclerView para mostrar um cabeçalho no início da lista. Nesse caso, o app vai usar um ViewHolder diferente para o cabeçalho e para os itens de dados. O app vai verificar o índice da lista para determinar qual ViewHolder usar.

Etapa 1: criar uma classe DataItem

Para abstrair o tipo de item e permitir que o adaptador lide apenas com "itens", crie uma classe de contêiner de dados que represente um SleepNight ou um Header. Seu conjunto de dados será uma lista de itens de detentor de dados.

Você pode acessar o app inicial no GitHub ou continuar usando o app SleepTracker criado no codelab anterior.

  1. Faça o download do código RecyclerViewHeaders-Starter no GitHub. O diretório RecyclerViewHeaders-Starter contém a versão inicial do app SleepTracker necessária para este codelab. Se preferir, você também pode continuar com o app concluído do codelab anterior.
  2. Abra SleepNightAdapter.kt.
  3. Abaixo da classe SleepNightListener, no nível superior, defina uma classe sealed chamada DataItem que representa um item de dados.

    Uma classe sealed define um tipo fechado, o que significa que todas as subclasses de DataItem precisam ser definidas nesse arquivo. Como resultado, o número de subclasses é conhecido pelo compilador. Não é possível que outra parte do seu código defina um novo tipo de DataItem que possa interromper seu adaptador.
sealed class DataItem {

 }
  1. No corpo da classe DataItem, defina duas classes que representam os diferentes tipos de itens de dados. A primeira é uma SleepNightItem, que é um wrapper em torno de uma SleepNight. Portanto, ela usa um único valor chamado sleepNight. Para que ela faça parte da classe selada, faça com que ela estenda DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. A segunda classe é Header, para representar um cabeçalho. Como um cabeçalho não tem dados reais, você pode declará-lo como um object. Isso significa que só haverá uma instância de Header. Mais uma vez, faça com que ele estenda DataItem.
object Header: DataItem()
  1. No DataItem, no nível da classe, defina uma propriedade abstract Long chamada id. Quando o adaptador usa DiffUtil para determinar se e como um item mudou, o DiffItemCallback precisa saber o ID de cada item. Você vai ver um erro porque SleepNightItem e Header precisam substituir a propriedade abstrata id.
abstract val id: Long
  1. Em SleepNightItem, substitua id para retornar o nightId.
override val id = sleepNight.nightId
  1. Em Header, substitua id para retornar Long.MIN_VALUE, que é um número muito, muito pequeno (literalmente, -2 elevado à potência de 63). Portanto, ele nunca vai entrar em conflito com nenhum nightId existente.
override val id = Long.MIN_VALUE
  1. O código finalizado vai ficar assim, e o app será criado sem erros.
sealed class DataItem {
    abstract val id: Long
    data class SleepNightItem(val sleepNight: SleepNight): DataItem()      {
        override val id = sleepNight.nightId
    }

    object Header: DataItem() {
        override val id = Long.MIN_VALUE
    }
}

Etapa 2: criar um ViewHolder para o cabeçalho

  1. Crie o layout do cabeçalho em um novo arquivo de recurso de layout chamado header.xml que mostre um TextView. Não há nada de interessante nisso, então aqui está o código.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Sleep Results"
    android:padding="8dp" />
  1. Extraia "Sleep Results" para um recurso de string e chame-o de header_text.
<string name="header_text">Sleep Results</string>
  1. Em SleepNightAdapter.kt, dentro de SleepNightAdapter, acima da classe ViewHolder, crie uma nova classe TextViewHolder. Essa classe aumenta o layout textview.xml e retorna uma instância TextViewHolder. Como você já fez isso antes, aqui está o código. Você precisará importar View e R:
    class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
        companion object {
            fun from(parent: ViewGroup): TextViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = layoutInflater.inflate(R.layout.header, parent, false)
                return TextViewHolder(view)
            }
        }
    }

Etapa 3: atualizar o SleepNightAdapter

Em seguida, atualize a declaração de SleepNightAdapter. Em vez de oferecer suporte a apenas um tipo de ViewHolder, ele precisa ser capaz de usar qualquer tipo de armazenador de visualização.

Definir os tipos de itens

  1. Em SleepNightAdapter.kt, no nível superior, abaixo das instruções import e acima de SleepNightAdapter, defina duas constantes para os tipos de visualização.

    O RecyclerView precisa distinguir o tipo de visualização de cada item para atribuir um holder de visualização corretamente.
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. Dentro do SleepNightAdapter, crie uma função para substituir getItemViewType() e retornar o cabeçalho ou a constante de item certa, dependendo do tipo do item atual.
override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
            is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
        }
    }

Atualize a definição de SleepNightAdapter

  1. Na definição de SleepNightAdapter, atualize o primeiro argumento para o ListAdapter de SleepNight para DataItem.
  2. Na definição de SleepNightAdapter, mude o segundo argumento genérico para ListAdapter de SleepNightAdapter.ViewHolder para RecyclerView.ViewHolder. Você vai encontrar alguns erros para atualizações necessárias, e o cabeçalho da sua classe vai ficar parecido com o mostrado abaixo.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Atualizar onCreateViewHolder()

  1. Mude a assinatura de onCreateViewHolder() para retornar um RecyclerView.ViewHolder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. Expanda a implementação do método onCreateViewHolder() para testar e retornar o suporte de visualização adequado para cada tipo de item. O método atualizado vai ficar assim:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
            ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
            else -> throw ClassCastException("Unknown viewType ${viewType}")
        }
    }

Atualizar onBindViewHolder()

  1. Mude o tipo de parâmetro de onBindViewHolder() de ViewHolder para RecyclerView.ViewHolder.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. Adicione uma condição para atribuir dados ao suporte de visualização somente se ele for um ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Faça o cast do tipo de objeto retornado por getItem() para DataItem.SleepNightItem. A função onBindViewHolder() concluída vai ficar assim:
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

Atualizar os callbacks do diffUtil

  1. Mude os métodos em SleepNightDiffCallback para usar a nova classe DataItem em vez de SleepNight. Suprima o aviso do lint conforme mostrado no código abaixo.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
    override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem.id == newItem.id
    }
    @SuppressLint("DiffUtilEquals")
    override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
        return oldItem == newItem
    }
}

Adicionar e enviar o cabeçalho

  1. No SleepNightAdapter, abaixo de onCreateViewHolder(), defina uma função addHeaderAndSubmitList(), conforme mostrado abaixo. Essa função usa uma lista de SleepNight. Em vez de usar submitList(), fornecido pelo ListAdapter, para enviar sua lista, use essa função para adicionar um cabeçalho e depois enviar a lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. Em addHeaderAndSubmitList(), se a lista transmitida for null, retorne apenas um cabeçalho. Caso contrário, anexe o cabeçalho ao início da lista e envie a lista.
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. Abra SleepTrackerFragment.kt e mude a chamada para submitList() para addHeaderAndSubmitList().
  1. Execute o app e observe como o cabeçalho é exibido como o primeiro item na lista de itens de sono.

Há duas coisas que precisam ser corrigidas neste app. Uma é visível, e a outra não.

  • O cabeçalho aparece no canto superior esquerdo e não é facilmente distinguível.
  • Não faz muita diferença para uma lista curta com um cabeçalho, mas não faça manipulação de listas em addHeaderAndSubmitList() na linha de execução da UI. Imagine uma lista com centenas de itens, vários cabeçalhos e uma lógica para decidir onde os itens precisam ser inseridos. Esse trabalho pertence a uma corrotina.

Mude addHeaderAndSubmitList() para usar corrotinas:

  1. No nível superior da classe SleepNightAdapter, defina um CoroutineScope com Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. Em addHeaderAndSubmitList(), inicie uma corrotina no adapterScope para manipular a lista. Em seguida, mude para o contexto Dispatchers.Main para enviar a lista, conforme mostrado no código abaixo.
 fun addHeaderAndSubmitList(list: List<SleepNight>?) {
        adapterScope.launch {
            val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
            withContext(Dispatchers.Main) {
                submitList(items)
            }
        }
    }
  1. O código vai ser criado e executado, e você não vai notar nenhuma diferença.

No momento, o cabeçalho tem a mesma largura dos outros itens na grade, ocupando um período horizontal e verticalmente. Toda a grade comporta três itens de uma largura de extensão horizontalmente. Portanto, o cabeçalho deve usar três extensões horizontais.

Para corrigir a largura do cabeçalho, você precisa informar ao GridLayoutManager quando abranger os dados em todas as colunas. Para isso, configure o SpanSizeLookup em um GridLayoutManager. É um objeto de configuração que o GridLayoutManager usa para determinar quantos intervalos usar para cada item na lista.

  1. Abra SleepTrackerFragment.kt.
  2. Encontre o código em que você define manager, perto do final de onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Abaixo de manager, defina manager.spanSizeLookup, conforme mostrado. Você precisa fazer uma object porque setSpanSizeLookup não usa uma lambda. Para criar um object em Kotlin, digite object : classname, neste caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Você pode receber um erro do compilador ao chamar o construtor. Se você fizer isso, abra o menu de intenção com Option+Enter (Mac) ou Alt+Enter (Windows) para aplicar a chamada do construtor.
  1. Em seguida, você vai receber um erro em object informando que precisa substituir métodos. Coloque o cursor em object, pressione Option+Enter (Mac) ou Alt+Enter (Windows) para abrir o menu de intenções e substitua o método getSpanSize().
  1. No corpo de getSpanSize(), retorne o tamanho de extensão correto para cada posição. A posição 0 tem um tamanho de extensão de 3, e as outras posições têm um tamanho de extensão de 1. O código concluído vai ficar assim:
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  1. Para melhorar a aparência do cabeçalho, abra header.xml e adicione este código ao arquivo de layout header.xml.
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
  1. Execute o app. Ele vai ficar parecido com a captura de tela abaixo.

Parabéns! Está tudo pronto.

Projeto do Android Studio: RecyclerViewHeaders (link em inglês)

  • Um cabeçalho geralmente é um item que abrange a largura de uma lista e funciona como um título ou separador. Uma lista pode ter um único cabeçalho para descrever o conteúdo do item ou vários cabeçalhos para agrupar e separar os itens.
  • Um RecyclerView pode usar vários armazenadores de visualização para acomodar um conjunto heterogêneo de itens, como cabeçalhos e itens de lista.
  • Uma maneira de adicionar cabeçalhos é modificar seu adaptador para usar um ViewHolder diferente, verificando os índices em que o cabeçalho precisa ser mostrado. O Adapter é responsável por acompanhar o cabeçalho.
  • Outra maneira de adicionar cabeçalhos é modificar o conjunto de dados de suporte (a lista) da grade de dados, que é o que você fez neste codelab.

Estas são as principais etapas para adicionar um cabeçalho:

  • Abstraia os dados na sua lista criando um DataItem que possa conter um cabeçalho ou dados.
  • Crie um fixador de visualização com um layout para o cabeçalho no adaptador.
  • Atualize o adaptador e os métodos dele para usar qualquer tipo de RecyclerView.ViewHolder.
  • Em onCreateViewHolder(), retorne o tipo correto de suporte de visualização para o item de dados.
  • Atualize SleepNightDiffCallback para trabalhar com a classe DataItem.
  • Crie uma função addHeaderAndSubmitList() que use corrotinas para adicionar o cabeçalho ao conjunto de dados e chame submitList().
  • Implemente GridLayoutManager.SpanSizeLookup() para que apenas o cabeçalho tenha três extensões de largura.

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

Quais das afirmações a seguir são verdadeiras sobre ViewHolder?

▢ Um adaptador pode usar várias classes ViewHolder para armazenar cabeçalhos e vários tipos de dados.

▢ Você pode ter exatamente um armazenador de visualização para os dados e outro para o cabeçalho.

▢ Um RecyclerView oferece suporte a vários tipos de cabeçalho, mas os dados precisam ser uniformes.

▢ Ao adicionar um cabeçalho, você cria uma subclasse RecyclerView para que ele seja inserido na posição correta.

Pergunta 2

Quando usar corrotinas com um RecyclerView? Selecione todas as afirmações verdadeiras.

▢ Nunca. Um RecyclerView é um elemento de interface do usuário e não deve usar corrotinas.

▢ Use corrotinas para tarefas de longa duração que podem deixar a interface lenta.

▢ As manipulações de lista podem levar muito tempo, e você sempre deve fazê-las usando corrotinas.

▢ Use corrotinas com funções de suspensão para evitar o bloqueio da linha de execução principal.

Pergunta 3

Qual das opções a seguir você NÃO precisa fazer ao usar mais de um ViewHolder?

▢ No ViewHolder, forneça vários arquivos de layout para inflar conforme necessário.

▢ Em onCreateViewHolder(), retorne o tipo correto de suporte de visualização para o item de dados.

▢ Em onBindViewHolder(), só vincule dados se o holder de visualização for o tipo correto para o item de dados.

▢ Generalize a assinatura da classe do adaptador para aceitar qualquer RecyclerView.ViewHolder.

Comece a próxima lição: 8.1 Como receber dados da Internet

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