Se você já precisou representar um conjunto finito de estados ou tipos no seu código, as Sealed Classes do Kotlin são exatamente o que você procura. Elas combinam o melhor dos enums com a flexibilidade das classes, e o compilador ainda te ajuda a não esquecer nenhum caso. Bora entender!

O que são Sealed Classes?

Uma sealed class é uma classe abstrata que restringe quais subclasses podem existir. Todas as subclasses devem ser definidas no mesmo pacote (e normalmente no mesmo arquivo). Isso dá ao compilador a garantia de que ele conhece todos os tipos possíveis.

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

Por que não usar Enum?

Enums são ótimos, mas têm uma limitação: cada valor é uma instância única. Você não pode ter dados diferentes em cada instância:

// Enum — cada valor é fixo
enum class Status { SUCESSO, ERRO, CARREGANDO }
// Como associar uma mensagem de erro ao Status.ERRO? Complicado.

// Sealed Class — cada subclasse pode ter seus próprios dados
sealed class Status {
    data class Sucesso(val dados: List<Item>) : Status()
    data class Erro(val exception: Throwable) : Status()
    data object Carregando : Status()
}

O poder do when exaustivo

A grande sacada das sealed classes é que o compilador sabe todas as subclasses. Quando você usa when, ele garante que todos os casos estão cobertos:

fun exibirStatus(status: Status): String = when (status) {
    is Status.Sucesso -> "Carregou ${status.dados.size} itens"
    is Status.Erro -> "Erro: ${status.exception.message}"
    is Status.Carregando -> "Carregando..."
    // Sem `else` necessário! O compilador sabe que cobriu tudo.
}

Se amanhã você adicionar um novo subtipo (digamos, Status.SemConexao), o compilador vai apontar erro em todos os when que não tratam esse novo caso. Isso é poderosíssimo para evitar bugs.

Caso prático: resultado de chamada de API

Esse é o uso mais clássico de sealed classes no desenvolvimento Android e backend:

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val message: String, val code: Int? = null) : ApiResult<Nothing>()
    data object Loading : ApiResult<Nothing>()
}

class UsuarioRepository(private val api: ApiService) {
    suspend fun buscarUsuario(id: Int): ApiResult<Usuario> {
        return try {
            val usuario = api.getUsuario(id)
            ApiResult.Success(usuario)
        } catch (e: HttpException) {
            ApiResult.Error("Erro HTTP: ${e.code()}", e.code())
        } catch (e: Exception) {
            ApiResult.Error("Falha na conexão: ${e.message}")
        }
    }
}

// No ViewModel
fun carregarUsuario(id: Int) {
    viewModelScope.launch {
        _uiState.value = ApiResult.Loading

        when (val resultado = repository.buscarUsuario(id)) {
            is ApiResult.Success -> _uiState.value = UiState.Dados(resultado.data)
            is ApiResult.Error -> _uiState.value = UiState.Erro(resultado.message)
            is ApiResult.Loading -> { /* já tratado acima */ }
        }
    }
}

Sealed classes para navegação

Definir rotas de navegação com sealed classes é um padrão muito usado em apps Android:

sealed class Tela(val rota: String) {
    data object Inicio : Tela("inicio")
    data object ListaProdutos : Tela("produtos")
    data class DetalhesProduto(val id: Int) : Tela("produtos/$id")
    data class Perfil(val userId: String) : Tela("perfil/$userId")
    data object Configuracoes : Tela("configuracoes")
}

fun navegar(tela: Tela, navController: NavController) {
    when (tela) {
        is Tela.Inicio -> navController.navigate(tela.rota)
        is Tela.ListaProdutos -> navController.navigate(tela.rota)
        is Tela.DetalhesProduto -> navController.navigate(tela.rota)
        is Tela.Perfil -> navController.navigate(tela.rota)
        is Tela.Configuracoes -> navController.navigate(tela.rota)
    }
}

Sealed interfaces

A partir do Kotlin 1.5, temos também sealed interfaces, que são ainda mais flexíveis pois uma classe pode implementar múltiplas sealed interfaces:

sealed interface Evento

sealed interface EventoDeUsuario : Evento {
    data class Login(val email: String) : EventoDeUsuario
    data class Logout(val motivo: String) : EventoDeUsuario
    data class AlterarPerfil(val campo: String, val valor: String) : EventoDeUsuario
}

sealed interface EventoDeSistema : Evento {
    data class ErroDeRede(val codigo: Int) : EventoDeSistema
    data object ConexaoRestabelecida : EventoDeSistema
}

fun tratarEvento(evento: Evento) = when (evento) {
    is EventoDeUsuario.Login -> println("Usuário logou: ${evento.email}")
    is EventoDeUsuario.Logout -> println("Logout: ${evento.motivo}")
    is EventoDeUsuario.AlterarPerfil -> println("Alterou ${evento.campo}")
    is EventoDeSistema.ErroDeRede -> println("Erro de rede: ${evento.codigo}")
    is EventoDeSistema.ConexaoRestabelecida -> println("Conexão OK!")
}

Modelando máquinas de estado

Sealed classes são perfeitas para máquinas de estado:

sealed class EstadoPedido {
    data object Criado : EstadoPedido()
    data class AguardandoPagamento(val valorTotal: Double) : EstadoPedido()
    data class Pago(val transacaoId: String) : EstadoPedido()
    data class EmSeparacao(val previsaoDespacho: LocalDate) : EstadoPedido()
    data class Enviado(val codigoRastreio: String) : EstadoPedido()
    data object Entregue : EstadoPedido()
    data class Cancelado(val motivo: String) : EstadoPedido()
}

fun proximaAcao(estado: EstadoPedido): String = when (estado) {
    is EstadoPedido.Criado -> "Aguardar pagamento"
    is EstadoPedido.AguardandoPagamento -> "Pagar R$ ${estado.valorTotal}"
    is EstadoPedido.Pago -> "Transação ${estado.transacaoId} confirmada. Separar produtos."
    is EstadoPedido.EmSeparacao -> "Despacho previsto: ${estado.previsaoDespacho}"
    is EstadoPedido.Enviado -> "Rastrear: ${estado.codigoRastreio}"
    is EstadoPedido.Entregue -> "Avaliar compra"
    is EstadoPedido.Cancelado -> "Pedido cancelado: ${estado.motivo}"
}

Sealed class vs sealed interface: quando usar qual?

  • Use sealed class quando as subclasses compartilham estado ou comportamento comum
  • Use sealed interface quando quer mais flexibilidade com múltipla implementação
  • Na dúvida, comece com sealed class — é mais simples de refatorar depois

Boas práticas

  1. Use sealed classes para estados de UI: Loading, Success, Error — o padrão clássico
  2. Prefira data class e data object para as subclasses — você ganha toString(), equals() e copy() de graça
  3. Evite else no when: perca o hábito de usar else com sealed classes; assim o compilador te avisa quando um novo caso não foi tratado
  4. Mantenha as subclasses simples: se uma subclasse está ficando complexa demais, talvez ela precise ser outra sealed class

Conclusão

Sealed Classes são um daqueles recursos que, depois que você começa a usar, não volta mais atrás. Elas tornam seu código mais seguro, mais expressivo e mais fácil de manter. O compilador vira seu aliado, garantindo que você nunca esqueça de tratar um caso.

Se você quer escrever Kotlin idiomático, dominar sealed classes é obrigatório. Mãos à obra!