Neste tutorial, você vai aprender a implementar o RecyclerView no Android usando Kotlin, desde a configuração básica até técnicas avançadas como DiffUtil, ListAdapter, múltiplos view types e tratamento de cliques. O RecyclerView é o componente mais importante para exibir listas no Android, e dominá-lo é essencial para qualquer desenvolvedor Android.
O que é o RecyclerView?
O RecyclerView é um componente do AndroidX que exibe grandes conjuntos de dados de forma eficiente, reciclando views que saem da tela para reutilizá-las com novos dados. Ele substitui o antigo ListView e oferece muito mais flexibilidade através de três componentes principais:
- Adapter: responsável por criar e vincular os dados às views
- ViewHolder: mantém referências às views de cada item, evitando chamadas repetidas a
findViewById - LayoutManager: define como os itens são posicionados (lista vertical, horizontal, grade, etc.)
Passo 1: Configurando o Projeto
Primeiro, adicione a dependência do RecyclerView no build.gradle.kts do módulo app:
dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
}
Crie o modelo de dados usando uma data class:
data class Contato(
val id: Long,
val nome: String,
val email: String,
val fotoUrl: String? = null
)
Passo 2: Criando o Layout do Item
Crie o arquivo item_contato.xml na pasta res/layout/:
// Referência do layout XML - item_contato.xml
// LinearLayout vertical com:
// - TextView para nome (id: tvNome)
// - TextView para email (id: tvEmail)
// - Padding de 16dp e margin bottom de 8dp
Na prática, você criará um XML com os componentes visuais necessários. Aqui focaremos no código Kotlin.
Passo 3: Implementando o ViewHolder e o Adapter
O ViewHolder encapsula a view de cada item, e o Adapter gerencia a criação e vinculação dos dados:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class ContatoAdapter(
private var contatos: List<Contato> = emptyList()
) : RecyclerView.Adapter<ContatoAdapter.ContatoViewHolder>() {
// ViewHolder mantém referências às views do item
class ContatoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvNome: TextView = itemView.findViewById(R.id.tvNome)
val tvEmail: TextView = itemView.findViewById(R.id.tvEmail)
fun bind(contato: Contato) {
tvNome.text = contato.nome
tvEmail.text = contato.email
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContatoViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contato, parent, false)
return ContatoViewHolder(view)
}
override fun onBindViewHolder(holder: ContatoViewHolder, position: Int) {
holder.bind(contatos[position])
}
override fun getItemCount(): Int = contatos.size
fun atualizarLista(novosContatos: List<Contato>) {
contatos = novosContatos
notifyDataSetChanged() // veremos uma alternativa melhor com DiffUtil
}
}
Passo 4: Configurando o RecyclerView na Activity/Fragment
Agora conectamos tudo na Activity ou Fragment:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class ContatosActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: ContatoAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contatos)
recyclerView = findViewById(R.id.rvContatos)
// LayoutManager define a disposição dos itens
recyclerView.layoutManager = LinearLayoutManager(this)
// Opcional: melhora performance quando os itens têm tamanho fixo
recyclerView.setHasFixedSize(true)
adapter = ContatoAdapter()
recyclerView.adapter = adapter
// Simula carregamento de dados
val contatos = listOf(
Contato(1, "Ana Silva", "ana@email.com"),
Contato(2, "Bruno Costa", "bruno@email.com"),
Contato(3, "Carla Dias", "carla@email.com")
)
adapter.atualizarLista(contatos)
}
}
O LayoutManager controla o posicionamento. As opções mais comuns são:
// Lista vertical (padrão)
LinearLayoutManager(context)
// Lista horizontal
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
// Grade com 2 colunas
GridLayoutManager(context, 2)
// Grade escalonada (estilo Pinterest)
StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
Passo 5: DiffUtil e ListAdapter — Atualizações Eficientes
Usar notifyDataSetChanged() é ineficiente porque recria todas as views. O DiffUtil calcula as diferenças entre duas listas e aplica apenas as mudanças necessárias com animações automáticas. O ListAdapter simplifica esse processo:
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
class ContatoDiffCallback : DiffUtil.ItemCallback<Contato>() {
override fun areItemsTheSame(oldItem: Contato, newItem: Contato): Boolean {
return oldItem.id == newItem.id // verifica se é o mesmo item
}
override fun areContentsTheSame(oldItem: Contato, newItem: Contato): Boolean {
return oldItem == newItem // verifica se o conteúdo mudou
}
}
class ContatoListAdapter(
private val onItemClick: (Contato) -> Unit
) : ListAdapter<Contato, ContatoListAdapter.ContatoViewHolder>(ContatoDiffCallback()) {
inner class ContatoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val tvNome: TextView = itemView.findViewById(R.id.tvNome)
private val tvEmail: TextView = itemView.findViewById(R.id.tvEmail)
init {
itemView.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onItemClick(getItem(position))
}
}
}
fun bind(contato: Contato) {
tvNome.text = contato.nome
tvEmail.text = contato.email
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContatoViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_contato, parent, false)
return ContatoViewHolder(view)
}
override fun onBindViewHolder(holder: ContatoViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
Para usar o ListAdapter, basta chamar submitList:
val adapter = ContatoListAdapter { contato ->
Toast.makeText(this, "Clicou em: ${contato.nome}", Toast.LENGTH_SHORT).show()
}
recyclerView.adapter = adapter
// Submete a lista — DiffUtil calcula as diferenças automaticamente
adapter.submitList(listaDeContatos)
// Atualiza com nova lista
adapter.submitList(novaLista)
Passo 6: Tratamento de Cliques
No exemplo acima já implementamos click handling via lambda no construtor do adapter. Aqui está um padrão mais completo com clique e clique longo:
class ContatoListAdapter(
private val onItemClick: (Contato) -> Unit,
private val onItemLongClick: (Contato) -> Boolean
) : ListAdapter<Contato, ContatoListAdapter.ContatoViewHolder>(ContatoDiffCallback()) {
inner class ContatoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// ... views
init {
itemView.setOnClickListener {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) onItemClick(getItem(pos))
}
itemView.setOnLongClickListener {
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) onItemLongClick(getItem(pos))
else false
}
}
// ... bind()
}
// ... onCreateViewHolder, onBindViewHolder
}
Passo 7: Múltiplos View Types
Quando a lista tem itens com layouts diferentes (por exemplo, cabeçalhos e itens normais), usamos múltiplos view types:
sealed class ListItem {
data class Cabecalho(val titulo: String) : ListItem()
data class ContatoItem(val contato: Contato) : ListItem()
}
class MultiTypeAdapter : ListAdapter<ListItem, RecyclerView.ViewHolder>(MultiDiffCallback()) {
companion object {
const val TIPO_CABECALHO = 0
const val TIPO_CONTATO = 1
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is ListItem.Cabecalho -> TIPO_CABECALHO
is ListItem.ContatoItem -> TIPO_CONTATO
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
TIPO_CABECALHO -> {
val view = inflater.inflate(R.layout.item_cabecalho, parent, false)
CabecalhoViewHolder(view)
}
TIPO_CONTATO -> {
val view = inflater.inflate(R.layout.item_contato, parent, false)
ContatoViewHolder(view)
}
else -> throw IllegalArgumentException("View type desconhecido: $viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is ListItem.Cabecalho -> (holder as CabecalhoViewHolder).bind(item)
is ListItem.ContatoItem -> (holder as ContatoViewHolder).bind(item.contato)
}
}
}
Passo 8: ItemDecoration para Separadores
O ItemDecoration permite adicionar separadores, margens e outros elementos visuais sem alterar o layout dos itens:
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class DivisorDecoration(
private val altura: Int = 2,
private val cor: Int = 0xFFE0E0E0.toInt()
) : RecyclerView.ItemDecoration() {
private val paint = Paint().apply { color = cor }
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
outRect.bottom = altura // espaço abaixo de cada item
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount - 1) {
val child = parent.getChildAt(i)
val top = child.bottom.toFloat()
c.drawRect(
child.left.toFloat(), top,
child.right.toFloat(), top + altura,
paint
)
}
}
}
// Uso:
recyclerView.addItemDecoration(DivisorDecoration())
// Ou use o DividerItemDecoration padrão do AndroidX:
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
Erros Comuns
Usar
notifyDataSetChanged()em vez de DiffUtil: Isso destrói todas as animações e recria todos os itens. Sempre prefira ListAdapter com DiffUtil para listas que mudam frequentemente.Acessar
adapterPositionno momento errado: A posição pode serRecyclerView.NO_POSITIONdurante animações. Sempre verifique antes de usar.Criar objetos dentro de
onBindViewHolder: Este método é chamado muitas vezes. Evite criar listeners, formatadores ou outros objetos aqui — mova-os paraonCreateViewHolderou para oinitdo ViewHolder.Esquecer
setHasFixedSize(true): Se todos os itens da lista têm o mesmo tamanho, ativar isso melhora significativamente a performance.Submeter a mesma referência de lista ao ListAdapter: O DiffUtil compara referências. Se você modificar a lista original e submeter novamente, nada será atualizado. Sempre crie uma nova lista com
toList()oulistOf().Não usar View Binding ou ViewBinding: Acessar views via
findViewByIdrepetidamente é ineficiente e propenso a erros. Considere usar View Binding para segurança de tipos.
Conclusão e Próximos Passos
Neste tutorial, você aprendeu a implementar o RecyclerView com Kotlin do básico ao avançado: criação do Adapter e ViewHolder, configuração de diferentes LayoutManagers, atualizações eficientes com DiffUtil e ListAdapter, tratamento de cliques com lambdas, múltiplos view types usando sealed classes, e personalização visual com ItemDecoration.
Como próximos passos, recomendamos:
- Explorar o Jetpack Compose como alternativa declarativa para listas com
LazyColumn - Aprender sobre paginação com a biblioteca Paging 3 para listas muito grandes
- Implementar swipe-to-delete e drag-and-drop com
ItemTouchHelper - Integrar o RecyclerView com MVVM para observar mudanças nos dados automaticamente
- Consultar o glossário de data class e lambda para reforçar conceitos usados neste tutorial
O RecyclerView continua sendo fundamental no desenvolvimento Android, e dominar suas funcionalidades avançadas é um diferencial importante na sua carreira.