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
- Use sealed classes para estados de UI:
Loading,Success,Error— o padrão clássico - Prefira
data classedata objectpara as subclasses — você ganhatoString(),equals()ecopy()de graça - Evite
elsenowhen: perca o hábito de usarelsecom sealed classes; assim o compilador te avisa quando um novo caso não foi tratado - 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!