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

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

Neste codelab, você aprenderá a adicionar um cabeçalho que abrange a largura da lista exibida em uma RecyclerView. Você se baseia no app monitor de sono dos codelabs anteriores.

O que você já precisa saber

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

O que você vai aprender

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

Atividades do laboratório

  • Crie no app TrackMySleepQuality do codelab anterior desta série.
  • Adicione um cabeçalho que abranja a largura da tela acima das noites de suspensão exibidas no RecyclerView.

O app do rastreador de sono inicial tem três 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 alguns 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, exibida no meio, é para selecionar uma classificação de qualidade do sono. A terceira tela é uma visualização detalhada que abre quando o usuário toca em um item na grade.

Esse app usa uma arquitetura simplificada com um controlador de IU, 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 exibidos. A tela principal final ficará assim:

Este codelab ensina o princípio geral de inclusão de itens que usam layouts diferentes em uma RecyclerView. Um exemplo comum é usar cabeçalhos na 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 o tipo de layout de cada item. O LayoutManager organiza os itens na tela, mas o adaptador adapta os dados a serem exibidos e transmite os armazenadores de visualização para o RecyclerView. Então, você adicionará o código para criar cabeçalhos no adaptador.

Duas formas 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 exibido. O Adapter será responsável por acompanhar o cabeçalho. Por exemplo, para mostrar um cabeçalho na parte superior da tabela, é preciso retornar uma ViewHolder diferente para o cabeçalho ao exibir o item indexado zero. Em seguida, todos os outros itens serão mapeados com o deslocamento do cabeçalho, como mostrado abaixo.

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

[0: cabeçalho]

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

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

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

Outra maneira de adicionar cabeçalhos é modificar o conjunto de dados de apoio para a grade de dados. Como todos os dados que precisam ser exibidos estão armazenados em uma lista, é possível modificar a lista para incluir itens que representem um cabeçalho. Esse código é um pouco mais simples de entender, mas exige que você pense em como projetar seus objetos para poder combinar os diferentes tipos de item em uma única lista. Implementado dessa maneira, o adaptador exibirá os itens transmitidos a ele. Portanto, 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: Cabeçalho] -> [0: Cabeçalho]

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

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

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

Cada metodologia tem benefícios e desvantagens. Alterar o conjunto de dados não introduz muita mudança no 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 ao verificar índices para cabeçalhos dá mais liberdade ao layout do cabeçalho. Ele também permite que o adaptador trate como os dados são adaptados para a visualização sem modificar os dados de apoio.

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

Etapa 1: criar uma classe DataItem

Para abstrair o tipo de item e deixar que o adaptador lide com "items", você pode criar uma classe armazenadora de dados que represente um SleepNight ou um Header. O conjunto de dados será uma lista dos itens armazenadores de dados.

Você pode fazer o download do 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. Você também pode continuar com o app finalizado do codelab anterior, se preferir.
  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 código defina um novo tipo de DataItem que possa quebrar o 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 da SleepNight, então ela usa um único valor chamado sleepNight. Para torná-la parte da classe selada, peça para ela estender 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 haverá apenas uma instância de Header. Novamente, peça que ele estenda o DataItem.
object Header: DataItem()
  1. Dentro de 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 foi modificado, o DiffItemCallback precisa saber o ID de cada item. Você verá um erro, porque SleepNightItem e Header precisam modificar a propriedade abstrata id.
abstract val id: Long
  1. Em SleepNightItem, substitua id para retornar o nightId.
override val id = sleepNight.nightId
  1. No Header, modifique id para retornar Long.MIN_VALUE, que é um número muito pequeno (literamente, -2 à potência de 63). Portanto, nunca haverá conflito com nenhum nightId existente.
override val id = Long.MIN_VALUE
  1. O código finalizado ficará da seguinte forma, 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 exibe 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" em um recurso de string e chame-o de header_text.
<string name="header_text">Sleep Results</string>
  1. Em SleepNightAdapter.kt, em SleepNightAdapter, acima da classe ViewHolder, crie uma nova classe TextViewHolder. Essa classe infla o layout textview.xml e retorna uma instância TextViewHolder. Como você já fez isso antes, veja o código abaixo, e 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 compatibilidade apenas com um tipo de ViewHolder, ele precisa ser compatível com 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 precisará distinguir cada tipo de visualização para cada item a fim de atribuir corretamente um armazenador de visualização.
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. No SleepNightAdapter, crie uma função para substituir getItemViewType() para retornar o cabeçalho ou a constante do item certo, 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
        }
    }

Atualizar a definição de SleepNightAdapter

  1. Na definição de SleepNightAdapter, atualize o primeiro argumento para a ListAdapter de SleepNight para DataItem.
  2. Na definição de SleepNightAdapter, mude o segundo argumento genérico de ListAdapter de SleepNightAdapter.ViewHolder para RecyclerView.ViewHolder. Você verá alguns erros para as atualizações necessárias, e o cabeçalho da sua turma terá a seguinte aparência:
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Atualizar onCreateViewHolder()

  1. Mude a assinatura de onCreateViewHolder() para retornar uma 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 armazenador de visualização adequado de cada tipo de item. O método atualizado ficará como o exemplo abaixo.
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 apenas ao armazenador de visualização se ele for uma ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Transmita o tipo de objeto retornado por getItem() como DataItem.SleepNightItem. A função onBindViewHolder() concluída ficará parecida com esta:
  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 de diffUtil

  1. Mude os métodos na 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 do método onCreateViewHolder(), defina uma função addHeaderAndSubmitList() como mostrado abaixo. Essa função usa uma lista de SleepNight. Em vez de usar submitList(), fornecido pelo ListAdapter, use essa função para adicionar um cabeçalho e enviar a lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. Dentro de addHeaderAndSubmitList(), se a lista transmitida for null, retorne apenas um cabeçalho. Caso contrário, anexe-o ao cabeçalho da lista e envie-a.
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 da lista.

Há duas coisas que precisam ser corrigidas para este app. Uma delas está visível, e a outra não.

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

Mude addHeaderAndSubmitList() para usar corrotinas:

  1. No nível superior dentro 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, alterne 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. Seu código será criado e executado, e você não verá diferença.

Atualmente, o cabeçalho tem a mesma largura que os outros itens na grade, ocupando um período horizontalmente e verticalmente. A grade inteira cabe em três itens de um período horizontalmente. Portanto, o cabeçalho precisa usar três períodos horizontalmente.

Para corrigir a largura do cabeçalho, é necessário informar ao GridLayoutManager quando incluir os dados em todas as colunas. Você pode fazer isso configurando o SpanSizeLookup em uma GridLayoutManager. Esse é um objeto de configuração que o GridLayoutManager usa para determinar quantos períodos usar para cada item na lista.

  1. Abra SleepTrackerFragment.kt.
  2. Encontre o código em que você define a manager, no final de onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Abaixo de manager, defina manager.spanSizeLookup, conforme mostrado. É necessário criar uma object, porque setSpanSizeLookup não usa um lambda. Para criar uma object em Kotlin, digite object : classname, neste caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. É possível que você receba um erro do compilador para chamar o construtor. Se você fizer isso, abra o menu da intent com Option+Enter (Mac) ou Alt+Enter (Windows) para aplicar a chamada de construtor.
  1. Em seguida, será exibido um erro no object informando que você precisa substituir os métodos. Coloque o cursor em object, pressione Option+Enter (Mac) ou Alt+Enter (Windows) para abrir o menu de intents e substitua o método getSpanSize().
  1. No corpo de getSpanSize(), retorne o tamanho do período correto para cada posição. A posição 0 tem um tamanho de período 3 e as outras posições têm tamanho 1. O código completo 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 esse 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 ficará parecido com a captura de tela abaixo.

Parabéns! Está tudo pronto.

Projeto do Android Studio: RecyclerViewHeaders

  • Um cabeçalho geralmente é um item que ocupa 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 dele ou vários cabeçalhos para agrupar itens e separá-los.
  • Uma RecyclerView pode usar vários armazenadores de visualização para acomodar um conjunto heterogêneo de itens. Por exemplo, cabeçalhos e itens de lista.
  • Uma maneira de adicionar cabeçalhos é modificar o adaptador para usar um ViewHolder diferente verificando os índices em que o cabeçalho precisa ser exibido. O Adapter é responsável por acompanhar o cabeçalho.
  • Outra forma de adicionar cabeçalhos é modificar o conjunto de dados de apoio (a lista) da sua grade de dados, que é o que você fez neste codelab.

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

  • Abstraia os dados em sua lista, criando um DataItem que possa conter um cabeçalho ou dados.
  • Criar um armazenador 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 armazenador de visualização do item de dados.
  • Atualize a SleepNightDiffCallback para que funcione 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 tornar apenas o cabeçalho com três períodos de largura.

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

Qual das seguintes afirmações sobre ViewHolder é verdadeira?

▢ 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 é compatível com vários tipos de cabeçalho, mas os dados precisam ser uniformes.

▢ Ao adicionar um cabeçalho, crie uma subclasse RecyclerView para inserir o cabeçalho na posição correta.

Pergunta 2

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

▢ Nunca. Um RecyclerView é um elemento de IU e não pode usar corrotinas.

▢ Usar corrotinas para tarefas de longa duração que podem atrasar a IU.

▢ As manipulações de lista podem demorar muito, e você deve sempre fazer isso usando corrotinas.

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

Pergunta 3

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

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

▢ Em onCreateViewHolder(), retorna o tipo correto de armazenador de visualização do item de dados.

▢ Em onBindViewHolder(), vincule os dados somente se o armazenador de visualização for o tipo correto do item de dados.

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

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

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.