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.
safeArgs
para passar dados entre fragmentos.
LiveData
e seus
observadores.
Room
, criar um DAO e definir entidades.RecyclerView
básico com um Adapter
,
ViewHolder
e layout de item.RecyclerView
.GridLayoutManager
.RecyclerView
.ViewHolder
com um RecyclerView
para adicionar itens
com um layout diferente. Especificamente, como usar um segundo ViewHolder
para
adicionar um cabeçalho acima dos itens exibidos em RecyclerView
. RecyclerView
. 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.
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.
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.
SleepNightListener
, no nível superior, defina uma classe
sealed
chamada DataItem
que representa um item de dados. 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 {
}
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()
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()
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
SleepNightItem
, substitua o id
para retornar o nightId
.
override val id = sleepNight.nightId
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
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
}
}
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" />
"Sleep Results"
em um recurso de string e chame-o de header_text
.
<string name="header_text">Sleep Results</string>
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)
}
}
}
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.
SleepNightAdapter.kt
, no nível superior, abaixo das instruções import
e
acima de SleepNightAdapter
, defina duas constantes para os tipos de vista. 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
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
}
}
SleepNightAdapter
, atualize o primeiro argumento para o
ListAdapter
de SleepNight
para DataItem
. 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()) {
onCreateViewHolder()
para retornar um
RecyclerView.ViewHolder
.override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
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}")
}
}
onBindViewHolder()
de ViewHolder
para
RecyclerView.ViewHolder
.override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
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)
}
}
}
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
}
}
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>?) {}
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)
submitList()
para
addHeaderAndSubmitList()
.Há dois erros que precisam ser corrigidos para este aplicativo. Um é visível e o outro não.
addHeaderAndSubmitList()
na thread de IU. Imagine uma lista com centenas de
itens, vários cabeçalhos e lógica para decidir onde os itens precisam ser inseridos. Este trabalho
pertence a uma corrotina.Altere addHeaderAndSubmitList()
para usar corrotinas:
SleepNightAdapter
, defina um
CoroutineScope
com Dispatchers.Default
. private val adapterScope = CoroutineScope(Dispatchers.Default)
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)
}
}
}
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.
manager
, próximo ao final de
onCreateView()
.
val manager = GridLayoutManager(activity, 3)
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() {
}
Option+Enter
(Mac) ou Alt+Enter
(Windows) para aplicar a chamada do
construtor.
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()
.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
}
}
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
Parabéns! Você terminou.
Projeto Android Studio: RecyclerViewHeaders
RecyclerView
pode usar vários recipientes de vistas para acomodar um conjunto
heterogêneo de itens; por exemplo, cabeçalhos e itens de lista. ViewHolder
diferente, verificando os índices onde seu cabeçalho precisa ser mostrado. O Adapter
é
responsável por manter o controle do cabeçalho.Estas são as principais etapas para adicionar um cabeçalho:
DataItem
que pode conter um cabeçalho ou
dados.
RecyclerView.ViewHolder
.
onCreateViewHolder()
, retorne o tipo correto de recipiente de vistas para o item de
dados.SleepNightDiffCallback
para trabalhar com a classe DataItem
.addHeaderAndSubmitList()
que usa corrotinas para adicionar o cabeçalho
ao conjunto de dados e depois chama submitList()
. GridLayoutManager.SpanSizeLookup()
para deixar apenas o cabeçalho com três
extensões de largura.
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.
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.
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.
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:
Para obter enlaces para outros tutoriais neste curso, consulte a página de destino dos tutoriais Fundamentos de Android em Kotlin.