O que e Supervisor em Kotlin?
No contexto de Kotlin Coroutines, o Supervisor e um mecanismo que modifica o comportamento padrao 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 nao afeta os irmaos – cada coroutine falha ou termina de forma independente.
Isso e essencial em cenarios onde tarefas sao independentes e a falha de uma nao deve derrubar as outras.
O problema: propagacao padrao de erros
Sem supervisor, o comportamento padrao 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 relacao 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 sao cancelados.supervisorScope: se um filho falha, os outros continuam.
Exemplo pratico: 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 usuario e estatisticas. Sem supervisor, a falha nas notificacoes derrubaria tudo.
SupervisorJob com CoroutineExceptionHandler
Com supervisor, excecoes nao propagam para o pai. Voce precisa de um CoroutineExceptionHandler para trata-las:
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, excecao ->
println("Excecao 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:
// Excecao capturada: Erro no filho 1
// Filho 2 completou normalmente
Sem o handler, a excecao seria impressa no stderr mas nao seria tratada. O handler permite logar, notificar ou tomar outras acoes.
Supervisor em ViewModels (Android)
O viewModelScope do Android usa SupervisorJob por padrao:
class MeuViewModel : ViewModel() {
// viewModelScope ja tem SupervisorJob
// Se uma coroutine falhar, as outras nao sao 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 | Criacao de CoroutineScope | Bloco suspend temporario |
| Duracao | Vive enquanto o scope existir | Dura ate todos os filhos terminarem |
| Completacao | Manual (cancel) | Automatica |
| Exception handler | Precisa de CoroutineExceptionHandler | Excecoes em await()/join() |
Quando usar Supervisor
- Tarefas independentes: quando multiplas operacoes sao lancadas em paralelo e a falha de uma nao deve afetar as outras.
- Dashboards e telas com dados de multiplas fontes: se uma fonte falha, as outras devem continuar.
- Processamento em lote: ao processar uma lista de itens, a falha em um nao deve parar o processamento dos outros.
- Servicos de longa duracao: workers ou servicos que lancam subtarefas independentes.
- ViewModels: escopos de ViewModel usam supervisor por padrao porque cada acao do usuario e independente.
Erros comuns
Usar SupervisorJob como pai direto de async sem tratar excecoes: com supervisor, excecoes em
asyncnao propagam automaticamente. Se voce nao chamarawait()ou nao tiver um handler, a excecao sera silenciosa.Confundir SupervisorJob com try-catch: o supervisor nao trata excecoes; ele apenas impede a propagacao. Voce ainda precisa tratar cada excecao individualmente.
Usar supervisor quando as tarefas sao dependentes: se o resultado de uma tarefa depende de outra, use
coroutineScopenormal. Nao faz sentido deixar uma continuar se sua dependencia falhou.Criar SupervisorJob sem handler: excecoes em coroutines filhas de um SupervisorJob que nao sao tratadas vao para o CoroutineExceptionHandler global. Configure um handler no scope.
Nao entender que supervisorScope espera todos os filhos:
supervisorScopeso 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 excecoes nao tratadas em coroutines.
- Structured Concurrency: principio de que coroutines formam hierarquias controladas.
- async/Deferred: lancamento de coroutine com resultado, onde excecoes sao capturadas em
await(). - viewModelScope: escopo do Android que usa SupervisorJob internamente.
O padrao 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 aplicacao mais robusta e seus usuarios mais satisfeitos.