O que é Data Class em Kotlin?

Uma data class em Kotlin é uma classe feita especialmente para armazenar dados. Quando você declara uma classe com o modificador data, o compilador gera automaticamente os métodos equals(), hashCode(), toString(), copy() e funções componentN() – tudo de graça.

Em Java, você precisaria escrever tudo isso na mão (ou usar uma biblioteca). Em Kotlin, basta uma linha.

Pense na data class como um formulário padronizado. Quando você preenche um formulário no cartório, os campos já estão definidos (nome, CPF, endereço) e dois formulários com os mesmos dados preenchidos são considerados equivalentes, independente de serem folhas de papel diferentes. A data class funciona da mesma forma: dois objetos com os mesmos valores são considerados iguais.

Sintaxe básica

data class Usuario(val nome: String, val email: String, val idade: Int)

fun main() {
    val user = Usuario("Mariana", "mari@email.com", 30)
    println(user) // Usuario(nome=Mariana, email=mari@email.com, idade=30)
}

Olha só: o toString() já vem formatado bonitinho, sem você fazer nada.

Comparação automática

Com data class, a comparação entre objetos é feita pelo conteúdo, não pela referência:

val a = Usuario("Lucas", "lucas@email.com", 25)
val b = Usuario("Lucas", "lucas@email.com", 25)

println(a == b)  // true — mesmo conteúdo
println(a === b) // false — objetos diferentes na memoria

A função copy()

Uma das funcionalidades mais práticas é o copy(), que cria uma cópia do objeto permitindo alterar apenas alguns campos:

val original = Usuario("Ana", "ana@email.com", 22)
val atualizado = original.copy(idade = 23)

println(atualizado) // Usuario(nome=Ana, email=ana@email.com, idade=23)

Isso é especialmente útil quando você trabalha com objetos imutáveis e precisa criar variações.

Destructuring

As funções componentN() geradas permitem usar destructuring:

val (nome, email, idade) = Usuario("Pedro", "pedro@email.com", 35)
println("$nome ($email) - $idade anos")

Regras importantes

  • O construtor primário precisa ter pelo menos um parâmetro.
  • Todos os parâmetros do construtor devem ser val ou var.
  • Data classes não podem ser abstract, open, sealed ou inner.

Data classes são perfeitas pra representar entidades, respostas de API, configurações e qualquer estrutura que carregue dados no seu projeto Kotlin.

Data class com propriedades fora do construtor

Propriedades declaradas no corpo da classe não participam de equals(), hashCode(), toString() e copy(). Isso é útil para campos computados ou auxiliares:

data class Produto(val nome: String, val preco: Double) {
    var desconto: Double = 0.0

    val precoFinal: Double
        get() = preco * (1 - desconto)
}

fun main() {
    val p1 = Produto("Notebook", 4500.0)
    val p2 = Produto("Notebook", 4500.0)
    p1.desconto = 0.10

    println(p1 == p2)          // true -- desconto nao participa do equals
    println(p1.precoFinal)     // 4050.0
    println(p2.precoFinal)     // 4500.0
}

Data class como modelo de API

Um cenário muito comum é usar data classes para representar respostas de APIs, combinando com serialização:

data class RespostaApi<T>(
    val sucesso: Boolean,
    val dados: T?,
    val mensagem: String,
    val timestamp: Long = System.currentTimeMillis()
)

data class Endereco(
    val rua: String,
    val cidade: String,
    val estado: String,
    val cep: String
)

fun main() {
    val endereco = Endereco("Rua das Flores", "São Paulo", "SP", "01001-000")
    val resposta = RespostaApi(
        sucesso = true,
        dados = endereco,
        mensagem = "Endereço encontrado"
    )

    println(resposta)
    // Acesso seguro aos dados
    resposta.dados?.let { println("Cidade: ${it.cidade}") }
}

Data class em coleções

Data classes funcionam perfeitamente com collections graças ao hashCode() e equals() gerados automaticamente:

data class Contato(val nome: String, val telefone: String)

fun main() {
    val agenda = mutableSetOf<Contato>()
    agenda.add(Contato("Ana", "11999990000"))
    agenda.add(Contato("Bruno", "11988880000"))
    agenda.add(Contato("Ana", "11999990000")) // duplicata ignorada pelo Set

    println(agenda.size) // 2

    val porLetra = agenda.groupBy { it.nome.first() }
    println(porLetra)
    // {A=[Contato(nome=Ana, telefone=11999990000)],
    //  B=[Contato(nome=Bruno, telefone=11988880000)]}
}

Casos de Uso no Mundo Real

  • DTOs (Data Transfer Objects): representar dados que trafegam entre camadas da aplicação, como entre repositório e viewmodel.
  • Respostas de API: modelar JSONs de APIs REST com campos tipados e valores padrão para campos opcionais.
  • Eventos de domínio: representar eventos em arquiteturas orientadas a eventos, como PedidoCriado(id, data, valor).
  • Chaves compostas em mapas: data classes funcionam como chaves de Map graças ao hashCode() consistente.
  • Estado de UI: em aplicações Android com Jetpack Compose, data classes representam o estado da tela (como UiState(loading, data, error)).

Boas Práticas

  • Prefira val sobre var nos parâmetros do construtor. Data classes imutáveis são mais seguras em ambientes concorrentes e mais previsíveis.
  • Use copy() para criar variações em vez de tornar propriedades mutáveis. Isso segue o princípio de imutabilidade.
  • Coloque valores padrão nos parâmetros para facilitar a criação de instâncias parciais e testes.
  • Não coloque lógica de negócio complexa dentro de data classes. Elas devem ser contêineres de dados simples.
  • Use sealed classes quando precisar de hierarquia: data classes não podem ser herdadas, mas podem ser subtipos de uma sealed class.

Erros Comuns

  • Declarar propriedades mutáveis que afetam igualdade: se você usa var no construtor, alterar um campo depois de adicionar o objeto a um Set ou como chave de um Map pode causar comportamento imprevisível, já que o hashCode muda.
  • Confundir propriedades do corpo com as do construtor: propriedades declaradas fora do construtor primário não participam de equals, hashCode, toString nem copy. Isso é intencional, mas pode surpreender.
  • Criar data classes com muitos parâmetros: se sua data class tem mais de 7-8 parâmetros, considere agrupar campos relacionados em data classes menores.
  • Esquecer que copy() faz cópia rasa: o copy() não clona objetos aninhados. Se um campo é uma MutableList, a cópia e o original compartilham a mesma lista.

Perguntas Frequentes

Qual a diferença entre data class e class normal? A data class gera automaticamente equals(), hashCode(), toString(), copy() e funções componentN(). Uma classe normal não gera nada disso – você precisaria implementar manualmente.

Posso herdar de uma data class? Não diretamente. Data classes são implicitamente final. Se você precisa de hierarquia, use uma sealed class como classe pai e data classes como subtipos.

Data class ou value class, quando usar cada uma? Use data class para estruturas com múltiplos campos. Use value class para envolver um único valor com significado semântico (como Email, Cpf), já que value classes têm otimização de memória.

O copy() faz cópia profunda? Não. O copy() faz cópia rasa (shallow copy). Propriedades que são objetos mutáveis (como listas mutáveis) serão compartilhadas entre o original e a cópia. Para cópia profunda, você precisa implementar manualmente.