O que é Supervisor em Kotlin?
No contexto de Kotlin Coroutines, o Supervisor é um mecanismo que modifica o comportamento padrão de propagacao de erros. Normalmente, quando uma coroutine filha falha, o erro propaga para o pai, que cancela todos os outros filhos. Com um Supervisor (via SupervisorJob ou supervisorScope), a falha de um filho não afeta os irmaos – cada coroutine falha ou termina de forma independente.
Isso e essencial em cenários onde tarefas são independentes é a falha de uma não deve derrubar as outras.
O problema: propagacao padrão de erros
Sem supervisor, o comportamento padrão e:
import kotlinx.coroutines.*
fun main() = runBlocking {
val scope = CoroutineScope(Job())
val job1 = scope.launch {
delay(1000)
println("Job 1 finalizado") // Nunca executa!
}
val job2 = scope.launch {
delay(500)
throw RuntimeException("Erro no job 2")
}
delay(2000)
println("Job 1 ativo: ${job1.isActive}") // false
println("Job 1 cancelado: ${job1.isCancelled}") // true -- cancelado pelo erro do irmao!
}
O erro no job2 propaga para o pai (o Job() do scope), que cancela job1 mesmo sem ter relação com o erro.
SupervisorJob
O SupervisorJob resolve isso:
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
val job1 = scope.launch {
delay(1000)
println("Job 1 finalizado com sucesso") // Executa normalmente!
}
val job2 = scope.launch {
delay(500)
throw RuntimeException("Erro no job 2")
}
delay(2000)
println("Job 1 completo: ${job1.isCompleted}") // true
println("Job 2 completo: ${job2.isCompleted}") // true (com excecao)
}
Com SupervisorJob, o job2 falha, mas o job1 continua normalmente.
supervisorScope
Para criar um escopo supervisor temporario dentro de uma coroutine:
suspend fun processarTodos(ids: List<Int>) = supervisorScope {
ids.map { id ->
async {
processar(id) // Se um falhar, os outros continuam
}
}.forEach { deferred ->
try {
val resultado = deferred.await()
println("Sucesso: $resultado")
} catch (e: Exception) {
println("Erro: ${e.message}")
}
}
}
suspend fun processar(id: Int): String {
if (id == 3) throw RuntimeException("Erro ao processar $id")
delay(100)
return "Processado: $id"
}
fun main() = runBlocking {
processarTodos(listOf(1, 2, 3, 4, 5))
}
// Saida:
// Sucesso: Processado: 1
// Sucesso: Processado: 2
// Erro: Erro ao processar 3
// Sucesso: Processado: 4
// Sucesso: Processado: 5
A diferenca entre coroutineScope e supervisorScope:
coroutineScope: se um filho falha, todos são cancelados.supervisorScope: se um filho falha, os outros continuam.
Exemplo prático: carregamento paralelo de dados
class DashboardService(
private val usuarioApi: UsuarioApi,
private val notificacaoApi: NotificacaoApi,
private val estatisticaApi: EstatisticaApi
) {
suspend fun carregarDashboard(userId: String): DashboardData = supervisorScope {
val usuario = async { usuarioApi.buscar(userId) }
val notificacoes = async { notificacaoApi.listar(userId) }
val estatisticas = async { estatisticaApi.buscar(userId) }
DashboardData(
usuario = tentarObter(usuario),
notificacoes = tentarObter(notificacoes) ?: emptyList(),
estatisticas = tentarObter(estatisticas)
)
}
private suspend fun <T> tentarObter(deferred: Deferred<T>): T? {
return try {
deferred.await()
} catch (e: Exception) {
null // Retorna null se falhar, nao cancela as outras
}
}
}
Aqui, se a API de notificacoes estiver fora do ar, o dashboard ainda carrega com os dados do usuário e estatisticas. Sem supervisor, a falha nas notificacoes derrubaria tudo.
SupervisorJob com CoroutineExceptionHandler
Com supervisor, exceções não propagam para o pai. Você precisa de um CoroutineExceptionHandler para trata-las:
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, excecao ->
println("Exceção capturada: ${excecao.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("Erro no filho 1")
}
scope.launch {
delay(1000)
println("Filho 2 completou normalmente")
}
delay(2000)
}
// Saida:
// Exceção capturada: Erro no filho 1
// Filho 2 completou normalmente
Sem o handler, a exceção seria impressa no stderr mas não seria tratada. O handler permite logar, notificar ou tomar outras acoes.
Supervisor em ViewModels (Android)
O viewModelScope do Android usa SupervisorJob por padrão:
class MeuViewModel : ViewModel() {
// viewModelScope ja tem SupervisorJob
// Se uma coroutine falhar, as outras nao são afetadas
fun carregarDados() {
viewModelScope.launch {
// Se falhar, nao afeta outras coroutines do scope
val dados = repositorio.buscar()
_estado.value = Estado.Sucesso(dados)
}
}
fun sincronizar() {
viewModelScope.launch {
// Independente do carregarDados
sincronizador.executar()
}
}
}
SupervisorJob vs supervisorScope
| Caracteristica | SupervisorJob | supervisorScope |
|---|---|---|
| Uso | Criação de CoroutineScope | Bloco suspend temporario |
| Duracao | Vive enquanto o scope existir | Dura até todos os filhos terminarem |
| Completacao | Manual (cancel) | Automatica |
| Exception handler | Precisa de CoroutineExceptionHandler | Exceções em await()/join() |
Quando usar Supervisor
- Tarefas independentes: quando múltiplas operações são lancadas em paralelo e a falha de uma não deve afetar as outras.
- Dashboards e telas com dados de múltiplas fontes: se uma fonte falha, as outras devem continuar.
- Processamento em lote: ao processar uma lista de itens, a falha em um não deve parar o processamento dos outros.
- Serviços de longa duracao: workers ou serviços que lancam subtarefas independentes.
- ViewModels: escopos de ViewModel usam supervisor por padrão porque cada acao do usuário e independente.
Casos de Uso no Mundo Real
Carregamento de dashboards e telas compostas: aplicativos que exibem dados de múltiplas fontes independentes (perfil do usuário, notificacoes, recomendacoes) utilizam
supervisorScopepara que a falha de uma API não impeca o carregamento das demais secoes. O usuário ve a tela parcialmente preenchida em vez de uma tela de erro completa.Processamento em lote de mensagens ou eventos: sistemas de backend que consomem filas de mensagens (Kafka, RabbitMQ) usam
SupervisorJobno escopo de processamento para que a falha ao processar uma mensagem não interrompa o consumo das demais. Cada mensagem e tratada de forma independente e falhas sao registradas para reprocessamento.Workers de sincronizacao em aplicativos mobile: aplicativos que sincronizam dados em segundo plano (fotos, contatos, arquivos) usam supervisor para que a falha na sincronizacao de um tipo de dado não cancele a sincronizacao dos demais. Por exemplo, se a sincronizacao de fotos falha por falta de espaco, os contatos continuam sincronizando.
Microservicos com health checks paralelos: servicos que verificam a saude de múltiplas dependências (banco de dados, cache, APIs externas) em paralelo usam supervisor para que a indisponibilidade de uma dependência não impeca a verificação das outras, permitindo relatorios parciais de saude.
Boas Praticas
- Sempre combine
SupervisorJobcom umCoroutineExceptionHandlerno escopo para capturar e registrar exceções de coroutines filhas. Sem o handler, exceções silenciosas podem passar despercebidas. - Prefira
supervisorScopepara escopos temporarios dentro de funções suspend, e reserveSupervisorJobpara escopos de longa duracao como ViewModels ou servicos. - Trate exceções individualmente em cada coroutine filha usando
try-catchem blocoslaunch, ou capture emawait()para blocosasync. O supervisor impede a propagacao, mas não substitui o tratamento de erro. - Nao use supervisor quando as tarefas sao dependentes entre si. Se o resultado de uma coroutine alimenta outra, use
coroutineScopepara que a falha cancele todas as dependentes. - Documente claramente no código por que um supervisor e usado em determinado ponto. Isso ajuda outros desenvolvedores a entender que as tarefas sao intencionalmente independentes e que falhas isoladas sao esperadas.
Perguntas Frequentes
P: Qual a diferenca entre usar SupervisorJob() e supervisorScope?
R: SupervisorJob() cria um Job que pode ser usado na construcao de um CoroutineScope de longa duracao. Ele permanece ativo até ser cancelado explicitamente. Ja supervisorScope cria um escopo temporario que aguarda todos os filhos terminarem e entao retorna. Use SupervisorJob() para escopos persistentes (como ViewModels) e supervisorScope para blocos isolados dentro de funções suspend.
P: O supervisor impede que exceções cheguem ao CoroutineExceptionHandler?
R: Nao. O supervisor impede que a exceção de um filho cancele seus irmaos e o pai, mas a exceção ainda e entregue ao CoroutineExceptionHandler configurado no escopo. Se nenhum handler estiver configurado, a exceção e reportada ao handler global da JVM.
P: Por que o viewModelScope do Android já usa SupervisorJob internamente?
R: Porque cada acao do usuário (clicar em um botao, navegar, buscar dados) lanca uma coroutine independente no ViewModel. Se uma dessas acoes falhar, não faz sentido cancelar todas as outras operações em andamento. O SupervisorJob garante esse isolamento naturalmente.
P: Posso usar supervisorScope dentro de um coroutineScope?
R: Sim. Você pode aninhar escopos livremente. O supervisorScope interno tera comportamento de supervisor para seus filhos diretos, enquanto o coroutineScope externo mantera o comportamento padrão de cancelamento para seus filhos. Isso e útil quando parte de uma operação tem tarefas independentes e o restante e dependente.
Erros comuns
Usar SupervisorJob como pai direto de async sem tratar exceções: com supervisor, exceções em
asyncnão propagam automaticamente. Se você não chamarawait()ou não tiver um handler, a exceção sera silenciosa.Confundir SupervisorJob com try-catch: o supervisor não trata exceções; ele apenas impede a propagacao. Você ainda precisa tratar cada exceção individualmente.
Usar supervisor quando as tarefas são dependentes: se o resultado de uma tarefa depende de outra, use
coroutineScopenormal. Nao faz sentido deixar uma continuar se sua dependência falhou.Criar SupervisorJob sem handler: exceções em coroutines filhas de um SupervisorJob que não são tratadas vao para o CoroutineExceptionHandler global. Configure um handler no scope.
Nao entender que supervisorScope espera todos os filhos:
supervisorScopesó retorna quando todos os filhos terminam (com sucesso ou falha). Se um filho trava, o scope inteiro trava.
Termos relacionados
- Job: elemento do contexto que representa o ciclo de vida de uma coroutine.
- CoroutineScope: escopo que define o ciclo de vida de um grupo de coroutines.
- CoroutineExceptionHandler: handler que captura exceções não tratadas em coroutines.
- Structured Concurrency: princípio de que coroutines formam hierarquias controladas.
- async/Deferred: lancamento de coroutine com resultado, onde exceções são capturadas em
await(). - viewModelScope: escopo do Android que usa SupervisorJob internamente.
O padrão Supervisor e uma ferramenta essencial para construir sistemas resilientes com Kotlin Coroutines. Ele permite que partes independentes do sistema falhem de forma isolada, sem derrubar todo o resto. Usar supervisor nos lugares certos torna sua aplicação mais robusta e seus usuários mais satisfeitos.