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
classregular — 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
Esquecer de usar
isno when: ao fazer pattern matching com sealed classes, cada branch precisa deispara verificar o tipo. Semis, o compilador tentará uma comparação de igualdade, que não é o desejado.Usar
elsedesnecessariamente: o poder das sealed classes está na verificação exaustiva. Adicionar um branchelsedesativa esse benefício — se você adicionar um novo subtipo, oelsevai capturá-lo silenciosamente em vez de gerar um erro de compilação.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 classou interface regular.Não aproveitar smart casts: dentro de um branch
isdowhen, Kotlin faz smart cast automaticamente. Você não precisa fazer cast manual para acessar propriedades do subtipo.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.