Neste tutorial, você vai aprender tudo sobre Sealed Classes em Kotlin — um recurso poderoso para representar hierarquias de tipos restritas. Sealed classes permitem que o compilador saiba exatamente quais subtipos existem, habilitando verificações exaustivas com when e padrões robustos de gerenciamento de estado e tratamento de erros. Ao final, você vai dominar sealed classes, sealed interfaces e saber aplicá-las em cenários reais.

O que São Sealed Classes?

Uma sealed class é uma classe abstrata cujas subclasses são conhecidas em tempo de compilação. Todas as subclasses diretas de uma sealed class devem ser declaradas no mesmo pacote (e, antes do Kotlin 1.5, no mesmo arquivo). Isso cria uma hierarquia de tipos fechada — nenhum código externo pode adicionar novos subtipos.

Pense em sealed classes como enums turbinadas. Enquanto enums definem um conjunto fixo de valores constantes, sealed classes definem um conjunto fixo de tipos, cada um podendo carregar dados diferentes.

Sintaxe Básica

sealed class Resultado {
    data class Sucesso(val dados: String) : Resultado()
    data class Erro(val mensagem: String, val codigo: Int) : Resultado()
    object Carregando : Resultado()
}

Cada subtipo pode ser:

  • Uma data class — quando precisa carregar dados
  • Uma class regular — quando tem comportamento próprio
  • Um object — quando representa um estado singleton (sem dados)

Expressões When com Sealed Classes

A maior vantagem de sealed classes aparece quando combinadas com expressões when. O compilador sabe exatamente quais subtipos existem e garante que você trate todos os casos:

fun processarResultado(resultado: Resultado): String {
    return when (resultado) {
        is Resultado.Sucesso -> "Dados: ${resultado.dados}"
        is Resultado.Erro -> "Erro ${resultado.codigo}: ${resultado.mensagem}"
        is Resultado.Carregando -> "Aguarde..."
        // Sem branch 'else' necessário! O compilador verifica exaustivamente.
    }
}

fun main() {
    val r1 = Resultado.Sucesso("Lista de usuários carregada")
    val r2 = Resultado.Erro("Não encontrado", 404)
    val r3 = Resultado.Carregando

    println(processarResultado(r1)) // Dados: Lista de usuários carregada
    println(processarResultado(r2)) // Erro 404: Não encontrado
    println(processarResultado(r3)) // Aguarde...
}

Se você adicionar um novo subtipo à sealed class, o compilador mostrará erros em todos os when que não tratam o novo caso. Isso é extremamente valioso para manutenção do código — bugs são capturados em tempo de compilação, não em tempo de execução.

Sealed Classes vs Enums

Embora pareçam similares, sealed classes e enums resolvem problemas diferentes:

// Enum: cada valor é uma instância ÚNICA e constante
enum class Direcao { NORTE, SUL, LESTE, OESTE }

// Sealed class: cada subtipo pode ter MÚLTIPLAS instâncias com dados diferentes
sealed class Evento {
    data class Click(val x: Int, val y: Int) : Evento()
    data class Tecla(val codigo: Int, val modificadores: Set<String>) : Evento()
    data class Scroll(val deltaX: Double, val deltaY: Double) : Evento()
    object Foco : Evento()
    object PerderaFoco : Evento()
}

fun main() {
    // Enum: só existe UM Direcao.NORTE
    val dir = Direcao.NORTE

    // Sealed: podem existir muitos Click diferentes
    val click1 = Evento.Click(100, 200)
    val click2 = Evento.Click(300, 400)
    // click1 e click2 são instâncias diferentes do mesmo tipo
}

Use enum quando: os valores são fixos e não carregam dados variáveis. Use sealed class quando: os tipos são fixos, mas cada instância pode ter dados próprios.

Sealed Interfaces (Kotlin 1.5+)

A partir do Kotlin 1.5, você pode declarar sealed interfaces. Isso é poderoso porque uma classe pode implementar múltiplas sealed interfaces, algo impossível com sealed classes (herança simples):

sealed interface Validavel {
    fun validar(): Boolean
}

sealed interface Serializavel {
    fun toJson(): String
}

// Uma classe pode implementar ambas as sealed interfaces
data class Email(val endereco: String) : Validavel, Serializavel {
    override fun validar(): Boolean = endereco.contains("@")
    override fun toJson(): String = """{"email": "$endereco"}"""
}

data class Telefone(val numero: String) : Validavel, Serializavel {
    override fun validar(): Boolean = numero.length >= 10
    override fun toJson(): String = """{"telefone": "$numero"}"""
}

data class Endereco(val rua: String) : Validavel {
    override fun validar(): Boolean = rua.isNotBlank()
}

fun processarValidavel(item: Validavel) {
    when (item) {
        is Email -> println("Email: ${item.endereco} — Válido: ${item.validar()}")
        is Telefone -> println("Tel: ${item.numero} — Válido: ${item.validar()}")
        is Endereco -> println("End: ${item.rua} — Válido: ${item.validar()}")
    }
}

Sealed interfaces expandem enormemente as possibilidades de modelagem de tipos em Kotlin.

Padrão de Gerenciamento de Estado

Sealed classes são ideais para representar estados de UI, especialmente em aplicações Android com ViewModel:

sealed class UiState<out T> {
    object Carregando : UiState<Nothing>()
    data class Sucesso<T>(val dados: T) : UiState<T>()
    data class Erro(val excecao: Throwable) : UiState<Nothing>()
    object Vazio : UiState<Nothing>()
}

// Simulação de ViewModel
class UsuarioViewModel {
    private var estado: UiState<List<String>> = UiState.Carregando

    fun carregarUsuarios() {
        estado = UiState.Carregando

        try {
            // Simulando chamada de API
            val usuarios = listOf("Ana", "Bruno", "Carla")
            estado = if (usuarios.isEmpty()) {
                UiState.Vazio
            } else {
                UiState.Sucesso(usuarios)
            }
        } catch (e: Exception) {
            estado = UiState.Erro(e)
        }
    }

    fun renderizar() {
        when (val s = estado) {
            is UiState.Carregando -> println("Exibindo spinner...")
            is UiState.Sucesso -> println("Usuários: ${s.dados.joinToString()}")
            is UiState.Erro -> println("Erro: ${s.excecao.message}")
            is UiState.Vazio -> println("Nenhum usuário encontrado")
        }
    }
}

fun main() {
    val viewModel = UsuarioViewModel()
    viewModel.renderizar()    // Exibindo spinner...
    viewModel.carregarUsuarios()
    viewModel.renderizar()    // Usuários: Ana, Bruno, Carla
}

Este padrão é amplamente usado em projetos Android modernos com Jetpack Compose e é considerado uma das melhores práticas para gerenciar estados de interface.

Padrão de Tratamento de Erros

Sealed classes oferecem uma alternativa elegante a exceptions para representar resultados que podem falhar:

sealed class Resultado<out T> {
    data class Ok<T>(val valor: T) : Resultado<T>()
    sealed class Falha : Resultado<Nothing>() {
        data class NaoEncontrado(val recurso: String) : Falha()
        data class SemPermissao(val acao: String) : Falha()
        data class ErroDeRede(val causa: Throwable) : Falha()
        data class Validacao(val campos: Map<String, String>) : Falha()
    }
}

fun buscarUsuario(id: Long): Resultado<String> {
    if (id <= 0) return Resultado.Falha.Validacao(mapOf("id" to "deve ser positivo"))
    if (id == 999L) return Resultado.Falha.NaoEncontrado("Usuário #$id")
    return Resultado.Ok("Usuário #$id encontrado")
}

fun main() {
    val ids = listOf(1L, -5L, 999L)

    ids.forEach { id ->
        when (val resultado = buscarUsuario(id)) {
            is Resultado.Ok -> println("Sucesso: ${resultado.valor}")
            is Resultado.Falha.NaoEncontrado -> println("404: ${resultado.recurso}")
            is Resultado.Falha.SemPermissao -> println("403: ${resultado.acao}")
            is Resultado.Falha.ErroDeRede -> println("Rede: ${resultado.causa.message}")
            is Resultado.Falha.Validacao -> println("Validação: ${resultado.campos}")
        }
    }
}

Note que sealed classes podem ter subclasses que também são sealed, criando hierarquias de tipos ricas e expressivas.

Sealed Classes com Generics

Combinando sealed classes com generics, você cria estruturas extremamente reutilizáveis:

sealed class Arvore<out T> {
    object Vazia : Arvore<Nothing>()
    data class No<T>(val valor: T, val esquerda: Arvore<T>, val direita: Arvore<T>) : Arvore<T>()
}

fun <T> Arvore<T>.tamanho(): Int = when (this) {
    is Arvore.Vazia -> 0
    is Arvore.No -> 1 + esquerda.tamanho() + direita.tamanho()
}

fun <T> Arvore<T>.toList(): List<T> = when (this) {
    is Arvore.Vazia -> emptyList()
    is Arvore.No -> esquerda.toList() + valor + direita.toList()
}

fun main() {
    val arvore = Arvore.No(
        10,
        Arvore.No(5, Arvore.Vazia, Arvore.Vazia),
        Arvore.No(15, Arvore.Vazia, Arvore.Vazia)
    )

    println("Tamanho: ${arvore.tamanho()}") // 3
    println("Elementos: ${arvore.toList()}") // [5, 10, 15]
}

Erros Comuns

  1. Esquecer de usar is no when: ao fazer pattern matching com sealed classes, cada branch precisa de is para verificar o tipo. Sem is, o compilador tentará uma comparação de igualdade, que não é o desejado.

  2. Usar else desnecessariamente: o poder das sealed classes está na verificação exaustiva. Adicionar um branch else desativa esse benefício — se você adicionar um novo subtipo, o else vai capturá-lo silenciosamente em vez de gerar um erro de compilação.

  3. Confundir sealed class com abstract class: sealed classes são restritas ao mesmo pacote. Se você precisa que código externo crie subtipos, use abstract class ou interface regular.

  4. Não aproveitar smart casts: dentro de um branch is do when, Kotlin faz smart cast automaticamente. Você não precisa fazer cast manual para acessar propriedades do subtipo.

  5. Criar hierarquias excessivamente profundas: sealed classes aninhadas em muitos níveis tornam o código difícil de manter. Mantenha a hierarquia rasa e clara.

Conclusão e Próximos Passos

Sealed classes são uma das ferramentas mais poderosas de Kotlin para modelagem de domínio. Elas garantem segurança em tempo de compilação, eliminam bugs de tipos não tratados e tornam o código expressivo e autodocumentável. Neste tutorial, você aprendeu a sintaxe, a integração com when, a diferença para enums, sealed interfaces, e padrões práticos de estado e erro.

Para aprofundar seus conhecimentos, explore:

  • Data Classes para usar como subtipos de sealed classes
  • Generics para criar sealed classes parametrizadas
  • Coroutines para usar sealed classes como resultado de operações assíncronas
  • Extension Functions para adicionar comportamento às suas sealed classes

Comece aplicando sealed classes nos seus projetos para representar estados de UI, resultados de operações e qualquer domínio com conjunto fixo de variantes. O compilador será seu aliado para manter o código seguro e completo.