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 umAdapter
,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 umaRecyclerView
para adicionar itens com um layout diferente. Especificamente, como usar um segundoViewHolder
para adicionar um cabeçalho acima dos itens mostrados emRecyclerView
.
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.
- 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.
- Abra SleepNightAdapter.kt.
- Abaixo da classe
SleepNightListener
, no nível superior, defina uma classesealed
chamadaDataItem
que representa um item de dados.
Uma classesealed
define um tipo fechado, o que significa que todas as subclasses deDataItem
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 deDataItem
que possa interromper seu adaptador.
sealed class DataItem {
}
- No corpo da classe
DataItem
, defina duas classes que representam os diferentes tipos de itens de dados. A primeira é umaSleepNightItem
, que é um wrapper em torno de umaSleepNight
. Portanto, ela usa um único valor chamadosleepNight
. Para que ela faça parte da classe selada, faça com que ela estendaDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- A segunda classe é
Header
, para representar um cabeçalho. Como um cabeçalho não tem dados reais, você pode declará-lo como umobject
. Isso significa que só haverá uma instância deHeader
. Mais uma vez, faça com que ele estendaDataItem
.
object Header: DataItem()
- No
DataItem
, no nível da classe, defina uma propriedadeabstract
Long
chamadaid
. Quando o adaptador usaDiffUtil
para determinar se e como um item mudou, oDiffItemCallback
precisa saber o ID de cada item. Você vai ver um erro porqueSleepNightItem
eHeader
precisam substituir a propriedade abstrataid
.
abstract val id: Long
- Em
SleepNightItem
, substituaid
para retornar onightId
.
override val id = sleepNight.nightId
- Em
Header
, substituaid
para retornarLong.MIN_VALUE
, que é um número muito, muito pequeno (literalmente, -2 elevado à potência de 63). Portanto, ele nunca vai entrar em conflito com nenhumnightId
existente.
override val id = Long.MIN_VALUE
- 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
- 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" />
- Extraia
"Sleep Results"
para um recurso de string e chame-o deheader_text
.
<string name="header_text">Sleep Results</string>
- Em SleepNightAdapter.kt, dentro de
SleepNightAdapter
, acima da classeViewHolder
, crie uma nova classeTextViewHolder
. Essa classe aumenta o layout textview.xml e retorna uma instânciaTextViewHolder
. Como você já fez isso antes, aqui está o código. Você precisará importarView
eR
:
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
- Em
SleepNightAdapter.kt
, no nível superior, abaixo das instruçõesimport
e acima deSleepNightAdapter
, defina duas constantes para os tipos de visualização.
ORecyclerView
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
- Dentro do
SleepNightAdapter
, crie uma função para substituirgetItemViewType()
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
- Na definição de
SleepNightAdapter
, atualize o primeiro argumento para oListAdapter
deSleepNight
paraDataItem
. - Na definição de
SleepNightAdapter
, mude o segundo argumento genérico paraListAdapter
deSleepNightAdapter.ViewHolder
paraRecyclerView.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()
- Mude a assinatura de
onCreateViewHolder()
para retornar umRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- 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()
- Mude o tipo de parâmetro de
onBindViewHolder()
deViewHolder
paraRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Adicione uma condição para atribuir dados ao suporte de visualização somente se ele for um
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Faça o cast do tipo de objeto retornado por
getItem()
paraDataItem.SleepNightItem
. A funçãoonBindViewHolder()
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
- Mude os métodos em
SleepNightDiffCallback
para usar a nova classeDataItem
em vez deSleepNight
. 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
- No
SleepNightAdapter
, abaixo deonCreateViewHolder()
, defina uma funçãoaddHeaderAndSubmitList()
, conforme mostrado abaixo. Essa função usa uma lista deSleepNight
. Em vez de usarsubmitList()
, fornecido peloListAdapter
, para enviar sua lista, use essa função para adicionar um cabeçalho e depois enviar a lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Em
addHeaderAndSubmitList()
, se a lista transmitida fornull
, 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)
- Abra SleepTrackerFragment.kt e mude a chamada para
submitList()
paraaddHeaderAndSubmitList()
.
- 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:
- No nível superior da classe
SleepNightAdapter
, defina umCoroutineScope
comDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- Em
addHeaderAndSubmitList()
, inicie uma corrotina noadapterScope
para manipular a lista. Em seguida, mude para o contextoDispatchers.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)
}
}
}
- 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.
- Abra SleepTrackerFragment.kt.
- Encontre o código em que você define
manager
, perto do final deonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Abaixo de
manager
, definamanager.spanSizeLookup
, conforme mostrado. Você precisa fazer umaobject
porquesetSpanSizeLookup
não usa uma lambda. Para criar umobject
em Kotlin, digiteobject : classname
, neste casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- 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) ouAlt+Enter
(Windows) para aplicar a chamada do construtor.
- Em seguida, você vai receber um erro em
object
informando que precisa substituir métodos. Coloque o cursor emobject
, pressioneOption+Enter
(Mac) ouAlt+Enter
(Windows) para abrir o menu de intenções e substitua o métodogetSpanSize()
.
- 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
}
}
- 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"
- 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. OAdapter
é 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 classeDataItem
. - Crie uma função
addHeaderAndSubmitList()
que use corrotinas para adicionar o cabeçalho ao conjunto de dados e chamesubmitList()
. - 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:
Para acessar links de outros codelabs neste curso, consulte a página inicial dos codelabs de conceitos básicos do Kotlin para Android.