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

CaracteristicaSupervisorJobsupervisorScope
UsoCriacao de CoroutineScopeBloco suspend temporario
DuracaoVive enquanto o scope existirDura ate todos os filhos terminarem
CompletacaoManual (cancel)Automatica
Exception handlerPrecisa de CoroutineExceptionHandlerExcecoes 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

  1. Usar SupervisorJob como pai direto de async sem tratar excecoes: com supervisor, excecoes em async nao propagam automaticamente. Se voce nao chamar await() ou nao tiver um handler, a excecao sera silenciosa.

  2. Confundir SupervisorJob com try-catch: o supervisor nao trata excecoes; ele apenas impede a propagacao. Voce ainda precisa tratar cada excecao individualmente.

  3. Usar supervisor quando as tarefas sao dependentes: se o resultado de uma tarefa depende de outra, use coroutineScope normal. Nao faz sentido deixar uma continuar se sua dependencia falhou.

  4. 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.

  5. Nao entender que supervisorScope espera todos os filhos: supervisorScope so 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.