---
title: "RecyclerView com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil"
url: "https://kotlin.dev.br/tutoriais/kotlin-recyclerview-tutorial/"
markdown_url: "https://kotlin.dev.br/tutoriais/kotlin-recyclerview-tutorial.MD"
description: "Aprenda RecyclerView com Kotlin: Adapter, ViewHolder, DiffUtil, ListAdapter, múltiplos view types e click handling com exemplos práticos."
date: "2025-07-06"
author: "Karina Melo"
---

# RecyclerView com Kotlin Tutorial em Português — Passo a Passo | Kotlin Brasil

Aprenda RecyclerView com Kotlin: Adapter, ViewHolder, DiffUtil, ListAdapter, múltiplos view types e click handling com exemplos práticos.


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:

```kotlin
dependencies {
    implementation("androidx.recyclerview:recyclerview:1.3.2")
}
```

Crie o modelo de dados usando uma [data class](/glossario/data-class/):

```kotlin
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/`:

```kotlin
// 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:

```kotlin
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:

```kotlin
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:

```kotlin
// Lista vertical (padrao)
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:

```kotlin
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 e 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`:

```kotlin
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](/glossario/lambda/) no construtor do adapter. Aqui está um padrão mais completo com clique e clique longo:

```kotlin
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:

```kotlin
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:

```kotlin
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 padrao 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](/glossario/lambda/), múltiplos view types usando [sealed classes](/glossario/sealed-class/), e personalização visual com ItemDecoration.

Como próximos passos, recomendamos:

- Explorar o [Jetpack Compose](/tutoriais/jetpack-compose-básico/) 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](/tutoriais/kotlin-mvvm-tutorial/) para observar mudanças nos dados automaticamente
- Consultar o [glossário de data class](/glossario/data-class/) e [lambda](/glossario/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.
