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 umAdapter
,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 umaRecyclerView
para adicionar itens com um layout diferente. Mais especificamente, como usar um segundoViewHolder
para adicionar um cabeçalho acima dos itens exibidos emRecyclerView
.
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.
- 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.
- 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 código defina um novo tipo deDataItem
que possa quebrar o 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 daSleepNight
, então ela usa um único valor chamadosleepNight
. Para torná-la parte da classe selada, peça para ela estenderDataItem
.
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 haverá apenas uma instância deHeader
. Novamente, peça que ele estenda oDataItem
.
object Header: DataItem()
- Dentro de
DataItem
, no nível da classe, defina uma propriedadeabstract
Long
chamadaid
. Quando o adaptador usaDiffUtil
para determinar se e como um item foi modificado, oDiffItemCallback
precisa saber o ID de cada item. Você verá um erro, porqueSleepNightItem
eHeader
precisam modificar a propriedade abstrataid
.
abstract val id: Long
- Em
SleepNightItem
, substituaid
para retornar onightId
.
override val id = sleepNight.nightId
- No
Header
, modifiqueid
para retornarLong.MIN_VALUE
, que é um número muito pequeno (literamente, -2 à potência de 63). Portanto, nunca haverá conflito com nenhumnightId
existente.
override val id = Long.MIN_VALUE
- 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
- 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" />
- Extraia
"Sleep Results"
em um recurso de string e chame-o deheader_text
.
<string name="header_text">Sleep Results</string>
- Em SleepNightAdapter.kt, em
SleepNightAdapter
, acima da classeViewHolder
, crie uma nova classeTextViewHolder
. Essa classe infla o layout textview.xml e retorna uma instânciaTextViewHolder
. Como você já fez isso antes, veja o código abaixo, e 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 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
- Em
SleepNightAdapter.kt
, no nível superior, abaixo das instruçõesimport
e acima deSleepNightAdapter
, defina duas constantes para os tipos de visualização.
ORecyclerView
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
- No
SleepNightAdapter
, crie uma função para substituirgetItemViewType()
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
- Na definição de
SleepNightAdapter
, atualize o primeiro argumento para aListAdapter
deSleepNight
paraDataItem
. - Na definição de
SleepNightAdapter
, mude o segundo argumento genérico deListAdapter
deSleepNightAdapter.ViewHolder
paraRecyclerView.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()
- Mude a assinatura de
onCreateViewHolder()
para retornar umaRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- 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()
- 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 apenas ao armazenador de visualização se ele for uma
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Transmita o tipo de objeto retornado por
getItem()
comoDataItem.SleepNightItem
. A funçãoonBindViewHolder()
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
- Mude os métodos na
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 do métodoonCreateViewHolder()
, defina uma funçãoaddHeaderAndSubmitList()
como mostrado abaixo. Essa função usa uma lista deSleepNight
. Em vez de usarsubmitList()
, fornecido peloListAdapter
, use essa função para adicionar um cabeçalho e enviar a lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Dentro de
addHeaderAndSubmitList()
, se a lista transmitida fornull
, 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)
- Abra SleepTrackerFragment.kt e mude a chamada para
submitList()
paraaddHeaderAndSubmitList()
.
- 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:
- No nível superior dentro 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, alterne 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)
}
}
}
- 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.
- Abra SleepTrackerFragment.kt.
- Encontre o código em que você define a
manager
, no final deonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Abaixo de
manager
, definamanager.spanSizeLookup
, conforme mostrado. É necessário criar umaobject
, porquesetSpanSizeLookup
não usa um lambda. Para criar umaobject
em Kotlin, digiteobject : classname
, neste casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- É 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) ouAlt+Enter
(Windows) para aplicar a chamada de construtor.
- Em seguida, será exibido um erro no
object
informando que você precisa substituir os métodos. Coloque o cursor emobject
, pressioneOption+Enter
(Mac) ouAlt+Enter
(Windows) para abrir o menu de intents e substitua o métodogetSpanSize()
.
- 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
}
}
- 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"
- 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. OAdapter
é 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 classeDataItem
. - Crie uma função
addHeaderAndSubmitList()
que use corrotinas para adicionar o cabeçalho ao conjunto de dados e chamesubmitList()
. - 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:
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.