Introdução

Neste tutorial, você aprende como adicionar um cabeçalho que abrange a largura da lista exibida em um RecyclerView. Você se baseia no aplicativo sleep-tracker de tutoriais anteriores.

O que você já deveria saber

O que aprenderá

O que fará

O aplicativo sleep-tracker com o qual você começa tem três 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 no meio, é para selecionar uma classificação de qualidade do sono. A terceira tela é uma vista de detalhes que se abre quando o usuário toca em um item na grade.

Este aplicativo usa uma arquitetura simplificada com um controlador de IU, modelo de vistas e LiveData e um banco de dados Room para persistir os dados de sono.

Neste tutorial, você adiciona um cabeçalho à grade de itens exibidos. Sua tela principal final será semelhante a esta:

Este tutorial ensina o princípio geral de inclusão de itens que usam layouts diferentes em um RecyclerView. Um exemplo comum é ter cabeçalhos em 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.

RecyclerView não sabe nada sobre seus dados ou que tipo de layout cada item possui. O LayoutManager organiza os itens na tela, mas o adaptador adapta os dados a serem exibidos e passa os recipientes de vistas para o RecyclerView. Portanto, você adicionará o código para criar cabeçalhos no adaptador.

Duas maneiras de adicionar cabeçalhos

Em RecyclerView, cada item na lista corresponde a um número de índice começando em 0. Por exemplo:

[Actual Data] -> [Adapter Views]

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

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

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

Uma maneira de adicionar cabeçalhos a uma lista é modificar seu adaptador para usar um ViewHolder diferente, verificando os índices onde seu cabeçalho precisa ser mostrado. O Adapter será responsável por manter o controle do cabeçalho. Por exemplo, para mostrar um cabeçalho no topo da tabela, você precisa retornar um ViewHolder diferente para o cabeçalho enquanto delineia o item indexado por zero. Em seguida, todos os outros itens seriam mapeados com o deslocamento do cabeçalho, conforme mostrado abaixo.

[Actual Data] -> [Adapter Views]

[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 apoio para sua grade de dados. Como todos os dados que precisam ser exibidos são armazenados em uma lista, você pode modificar a lista para incluir itens que representem um cabeçalho. Isso é um pouco mais simples de entender, mas requer que você pense sobre como criar seus objetos, para que possa combinar os diferentes tipos de itens em uma única lista. Implementado dessa forma, o adaptador exibirá os itens passados ​​para ele. Portanto, o item na posição 0 é um cabeçalho e o item na posição 1 é um SleepNight, que mapeia diretamente para o que está na tela.

[Actual Data] -> [Adapter Views]

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

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

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

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

Cada metodologia tem vantagens e desvantagens. Alterando o conjunto de dados não introduz muita mudança no resto 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, oferece mais liberdade no layout do cabeçalho. Ele também permite que o adaptador controle como os dados são adaptados à vista sem modificar os dados de apoio.

Neste tutorial, você atualiza seu RecyclerView para exibir um cabeçalho no início da lista. Nesse caso, seu aplicativo usará um ViewHolder diferente para o cabeçalho do que para itens de dados. O aplicativo verificará o índice da lista para determinar qual ViewHolder usar.

Etapa 1: Crie uma classe DataItem

Para abstrair o tipo de item e permitir que o adaptador trate apenas com "itens", você pode criar uma classe de recipiente de dados que representa um SleepNight ou um Header. Seu conjunto de dados será então uma lista de itens de recipiente de dados.

Você pode obter o aplicativo inicial do GitHub ou continuar usando o aplicativo SleepTracker que você criou no tutorial anterior.

  1. Baixe o código RecyclerViewHeaders-Starter do GitHub. O diretório RecyclerViewHeaders-Starter contém a versão inicial do aplicativo SleepTracker necessária para este tutorial. Você também pode continuar com o aplicativo concluído do tutorial 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 devem ser definidas neste arquivo. Como resultado, o número de subclasses é conhecido pelo compilador. Não é possível para outra parte do seu código definir um novo tipo de DataItem que poderia quebrar o seu adaptador.
sealed class DataItem {

 }
  1. Dentro do corpo da classe DataItem, defina duas classes que representam os diferentes tipos de itens de dados. O primeiro é um SleepNightItem, que é um embrulho em torno de um SleepNight, portanto, leva um único valor chamado sleepNight. Para torná-lo parte da classe lacrada, estenda o DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. A segunda classe é Header, para representar um cabeçalho. Como um cabeçalho não possui dados reais, você pode declará-lo como um object. Isso significa que haverá apenas uma instância do Header. Novamente, faça com que ele estenda DataItem.
object Header: DataItem()
  1. Dentro do 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 alterado, o DiffItemCallback precisa saber a id de cada item. Você verá um erro, pois SleepNightItem e Header precisam substituir a propriedade abstrata id.
abstract val id: Long
  1. Em SleepNightItem, substitua o id para retornar o nightId.
override val id = sleepNight.nightId
  1. No Header, substitua id para retornar Long.MIN_VALUE, que é um número muito, muito pequeno (literalmente, -2 à potência de 63). Portanto, isso nunca entrará em conflito com qualquer nightId existente.
override val id = Long.MIN_VALUE
  1. Seu código final deve ser parecido com este e seu aplicativo deve ser compilado 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: Crie um ViewHolder para o cabeçalho

  1. Crie o layout para o cabeçalho em um novo arquivo de recursos de layout chamado header.xml que exibe um TextView. Não há nada de empolgante nisso, então aqui está o código.

<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, dentro de SleepNightAdapter, acima da classe ViewHolder, crie uma classe TextViewHolder. Esta classe infla o layout textview.xml e retorna uma instância de TextViewHolder. Como você já fez isso antes, aqui está o código e você terá que 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: Atualize o SleepNightAdapter

Em seguida, você precisa atualizar a declaração de SleepNightAdapter. Em vez de suportar apenas um tipo de ViewHolder, ele precisa ser capaz de usar qualquer tipo de recipiente de vistas.

Defina 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 vista.

    O RecyclerView precisará distinguir o tipo de vistas de cada item, para que possa atribuir corretamente um recipiente de vistas a ele.
    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() para retornar o cabeçalho correto ou constante do item, 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 do SleepNightAdapter

  1. Na definição de SleepNightAdapter, atualize o primeiro argumento para o ListAdapter de SleepNight para DataItem.
  2. Na definição de SleepNightAdapter, altere o segundo argumento genérico para o ListAdapter de SleepNightAdapter.ViewHolder para RecyclerView.ViewHolder. Você verá alguns erros para as atualizações necessárias e o cabeçalho da classe deve ser semelhante ao mostrado abaixo.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Atualize onCreateViewHolder()

  1. Altere 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 recipiente de vistas apropriado para cada tipo de item. Seu método atualizado deve ser semelhante ao código 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}")
        }
    }

Atualize onBindViewHolder()

  1. Altere 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 recipiente da vista se o recipiente for um ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Converta o tipo de objeto retornado por getItem() em DataItem.SleepNightItem. Sua função onBindViewHolder() finalizada deve se parecer com isto.
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

Atualize os retornos de chamada diffUtil

  1. Altere os métodos em SleepNightDiffCallback para usar sua nova classe DataItem em vez de SleepNight. Suprima o aviso de 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
    }
}

Adicione e envie o cabeçalho

  1. Dentro do SleepNightAdapter, abaixo de onCreateViewHolder(), defina uma função addHeaderAndSubmitList() conforme mostrado abaixo. Esta função obtém uma lista de SleepNight. Em vez de usar submitList(), fornecido pelo ListAdapter, para enviar sua lista, você usará esta função para adicionar um cabeçalho e, em seguida, enviar a lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. Dentro de addHeaderAndSubmitList(), se a lista passada for null, retorne apenas um cabeçalho, caso contrário, anexe a cabeça ao cabeçalho da lista e, em seguida, 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 altere a chamada para submitList() para addHeaderAndSubmitList().
  1. Execute seu aplicativo e observe como seu cabeçalho é exibido como o primeiro item na lista de itens de sono.

Há dois erros que precisam ser corrigidos para este aplicativo. Um é visível e o outro não.

Altere 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, 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. Seu código deve ser construído e executado, e você não verá nenhuma diferença.

Atualmente, o cabeçalho tem a mesma largura que os outros itens da grade, ocupando uma extensão horizontal e verticalmente. A grade inteira se encaixa três itens de uma largura de vão horizontalmente, portanto, o cabeçalho deve usar três vãos horizontalmente.

Para fixar a largura do cabeçalho, você precisa dizer ao GridLayoutManager quando distribuir os dados por todas as colunas. Você pode fazer isso configurando o SpanSizeLookup em um GridLayoutManager. Este é um objeto de configuração que o GridLayoutManager usa para determinar quantos spans usar para cada item na lista.

  1. Abra SleepTrackerFragment.kt.
  2. Encontre o código onde você define manager, próximo ao final de onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Abaixo de manager, defina manager.spanSizeLookup, como mostrado. Você precisa fazer um object, pois setSpanSizeLookup não aceita um lambda. Para fazer um object em Kotlin, digite object : Classname, neste caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Você pode obter um erro do compilador ao chamar o construtor. Se o fizer, abra o menu de intenção com Option+Enter (Mac) ou Alt+Enter (Windows) para aplicar a chamada do construtor.
  1. Então você obterá um erro em object dizendo que você precisa sobrescrever os métodos. Coloque o cursor no object, pressione Option+Enter (Mac) ou Alt+Enter (Windows) para abrir o menu de intenções e, em seguida, substitua o método getSpanSize().
  1. No corpo de getSpanSize(), retorne o tamanho correto do span para cada posição. A posição 0 tem um tamanho de intervalo de 3 e as outras posições possuem um tamanho de intervalo de 1. Seu código concluído deve ser semelhante ao código abaixo:
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  1. Para melhorar a aparência do seu 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 seu aplicativo. Deve ser semelhante à imagem abaixo.

Parabéns! Você terminou.

Projeto Android Studio: RecyclerViewHeaders

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

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

Qual das afirmações a seguir é verdadeira 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 recipiente de vistas para dados e um recipiente de vistas para um cabeçalho.

▢ Um RecyclerView suporta vários tipos de cabeçalhos, mas os dados devem ser uniformes.

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

Pergunta 2

Quando você deve usar corrotinas com um RecyclerView? Selecione todas as afirmações que são verdadeiras.

▢ Nunca. Um RecyclerView é um elemento da IU e não deve usar corrotinas.

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

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

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

Pergunta 3

Qual das seguintes opções 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 recipiente de vistas para o item de dados.

▢ Em onBindViewHolder(), apenas vincule os dados se o recipiente de vistas for o tipo correto de recipiente de vistas para o item de dados.

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

Comece a próxima lição: 08.1: Obtendo dados da internet

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