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

  1. 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.

  2. Acessar adapterPosition no momento errado: A posição pode ser RecyclerView.NO_POSITION durante animações. Sempre verifique antes de usar.

  3. Criar objetos dentro de onBindViewHolder: Este método é chamado muitas vezes. Evite criar listeners, formatadores ou outros objetos aqui — mova-os para onCreateViewHolder ou para o init do ViewHolder.

  4. Esquecer setHasFixedSize(true): Se todos os itens da lista têm o mesmo tamanho, ativar isso melhora significativamente a performance.

  5. 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() ou listOf().

  6. Não usar View Binding ou ViewBinding: Acessar views via findViewById repetidamente é 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.