O que é Job em Kotlin?
O Job é o elemento do contexto de uma coroutine que representa seu ciclo de vida. Ele permite controlar a execução da coroutine: verificar se esta ativa, esperar sua conclusão ou cancela-la. Todo launch retorna um Job, e todo async retorna um Deferred (que é um subtipo de Job com resultado).
Pense no Job como a “alca” que você segura para controlar uma coroutine que esta rodando em background.
Sintaxe básica
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(2000)
println("Coroutine finalizada")
}
println("Job ativo? ${job.isActive}") // true
println("Job completo? ${job.isCompleted}") // false
job.join() // Espera a coroutine terminar
println("Job ativo? ${job.isActive}") // false
println("Job completo? ${job.isCompleted}") // true
}
O método join() suspende a coroutine atual até que o job termine. Diferente de Thread.join(), ele não bloqueia a thread.
Estados do Job
Um Job passa por vários estados durante sua vida:
New -> Active -> Completing -> Completed
|
v
Cancelling -> Cancelled
fun main() = runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
println("Executando...")
delay(1000)
println("Finalizado")
}
println("Novo: isActive=${job.isActive}") // false
job.start()
println("Ativo: isActive=${job.isActive}") // true
job.join()
println("Completo: isCompleted=${job.isCompleted}") // true
}
Com CoroutineStart.LAZY, o job começa no estado “New” e só entra em “Active” quando você chama start() ou join().
Cancelamento de Jobs
O cancelamento e cooperativo em Kotlin. Chamar cancel() não mata a coroutine instantaneamente – ela precisa verificar o cancelamento:
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("Trabalhando... $i")
delay(100) // Ponto de suspensao: verifica cancelamento
}
}
delay(500)
println("Cancelando...")
job.cancel()
job.join() // Espera a coroutine finalizar o cancelamento
// Ou: job.cancelAndJoin() // Combina cancel() + join()
println("Cancelado")
}
Funções como delay, yield e withContext verificam o cancelamento automaticamente. Se sua coroutine faz trabalho intensivo de CPU sem pontos de suspensao, use isActive ou ensureActive():
val job = launch(Dispatchers.Default) {
var i = 0
while (isActive) { // Verifica cancelamento manualmente
i++
// Trabalho intensivo de CPU
}
println("Parou em $i")
}
Hierarquia de Jobs (Structured Concurrency)
Jobs formam uma hierarquia pai-filho. Quando uma coroutine lanca outra, o job filho e registrado no job pai:
fun main() = runBlocking {
val jobPai = launch {
val jobFilho1 = launch {
delay(2000)
println("Filho 1 finalizado")
}
val jobFilho2 = launch {
delay(1000)
println("Filho 2 finalizado")
}
println("Filhos lancados")
}
delay(500)
jobPai.cancel() // Cancela pai E todos os filhos
jobPai.join()
println("Tudo cancelado")
}
Regras da hierarquia:
- Cancelar o pai cancela todos os filhos.
- Uma exceção não tratada em um filho cancela o pai e todos os irmaos.
- O pai só e considerado completo quando todos os filhos terminam.
Job vs Deferred
Deferred<T> e um Job que retorna um resultado:
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(1000)
42
}
// Deferred tem todas as funcionalidades de Job
println("Ativo: ${deferred.isActive}")
// Mais: pode obter o resultado
val resultado = deferred.await()
println("Resultado: $resultado")
}
Use launch (Job) quando não precisa de resultado; use async (Deferred) quando precisa.
SupervisorJob
O SupervisorJob modifica o comportamento de propagacao de erros: a falha de um filho não cancela os irmaos:
fun main() = runBlocking {
val supervisor = SupervisorJob()
val scope = CoroutineScope(coroutineContext + supervisor)
val job1 = scope.launch {
delay(1000)
throw RuntimeException("Erro no job 1")
}
val job2 = scope.launch {
delay(2000)
println("Job 2 finalizado normalmente") // Executa mesmo com erro no job1
}
delay(3000)
supervisor.cancel()
}
SupervisorJob e essencial em cenários onde tarefas são independentes e a falha de uma não deve afetar as outras.
Completable Job
CompletableJob e um Job que pode ser completado manualmente:
fun main() = runBlocking {
val job = Job() // Cria um CompletableJob
launch(job) {
delay(1000)
println("Tarefa executada")
}
job.complete() // Marca como completando (nao aceita mais filhos)
job.join() // Espera filhos existentes terminarem
println("Tudo feito")
}
Quando usar Job diretamente
- Cancelamento controlado: quando você precisa cancelar uma operação específica em resposta a uma acao do usuário.
- Esperar conclusão: quando uma parte do código depende de outra coroutine terminar antes de continuar.
- Monitorar estado: quando você precisa saber se uma coroutine ainda esta rodando.
- Escopo customizado: criar CoroutineScopes com Jobs específicos para controlar grupos de coroutines.
Casos de Uso no Mundo Real
Cancelamento de requisicoes de rede em Android: Quando o usuário navega para outra tela, o Job associado ao CoroutineScope da tela anterior e cancelado, abortando automaticamente todas as chamadas de API em andamento. Isso evita vazamentos de memória e atualizações de UI em telas que já não existem.
Processamento paralelo com controle de progresso: Em aplicações que processam lotes de dados (como upload de múltiplas imagens), cada tarefa e lancada como um Job filho. O Job pai permite monitorar o progresso geral, cancelar tudo se necessário, e garantir que o processo só e considerado completo quando todas as imagens foram enviadas.
Tarefas de background com timeout: Servicos que consultam APIs externas utilizam Jobs com
withTimeoutpara garantir que uma requisicao lenta não bloqueie o sistema indefinidamente. O Job e cancelado automaticamente apos o tempo limite, liberando recursos.Workers independentes com SupervisorJob: Em servidores backend com Ktor ou Spring, cada requisicao HTTP e tratada por um Job independente sob um SupervisorJob. Se uma requisicao falha com exceção, as demais continuam operando normalmente sem serem afetadas.
Boas Praticas
- Use
cancelAndJoin()em vez de chamarcancel()ejoin()separadamente para garantir que o cancelamento seja concluido antes de prosseguir. - Sempre verifique
isActiveou chameensureActive()em loops de CPU intensivo para que o cancelamento cooperativo funcione corretamente. - Prefira
SupervisorJobquando as coroutines filhas sao independentes e a falha de uma não deve afetar as demais. - Nunca capture
CancellationExceptionem blocoscatchgenericos. Se precisar capturarException, relanceCancellationExceptionexplicitamente. - Estruture suas coroutines com escopos adequados (como
viewModelScopeoulifecycleScopeno Android) em vez de usarGlobalScope, para que os Jobs sejam gerenciados automaticamente pelo ciclo de vida.
Perguntas Frequentes
P: Qual a diferenca entre Job e Deferred?
R: Job representa uma coroutine que executa uma tarefa sem retornar resultado, criado com launch. Deferred e um subtipo de Job que carrega um valor de retorno, criado com async. Voce obtém o resultado de um Deferred chamando await(), enquanto com Job você apenas espera a conclusao com join().
P: O que acontece quando um Job filho lanca uma exceção? R: Com um Job normal, a exceção propaga para o Job pai, que cancela todos os outros filhos e a si mesmo. Com SupervisorJob, a exceção fica isolada no filho que falhou, e os demais continuam executando normalmente.
P: Por que o cancelamento de coroutines e cooperativo? R: O cancelamento cooperativo e uma decisao de design que evita problemas como corrupcao de dados e vazamento de recursos. Se uma coroutine fosse interrompida abruptamente, ela poderia deixar arquivos abertos ou transacoes incompletas. O modelo cooperativo permite que a coroutine finalize suas operações de limpeza antes de parar.
P: Posso reutilizar um Job depois de cancelado?
R: Nao. Um Job cancelado ou completado entra em um estado terminal e não pode ser reiniciado. Se você precisa executar a tarefa novamente, crie um novo Job com um novo launch ou async.
Erros comuns
Nao chamar join apos cancel:
cancel()apenas sinaliza o cancelamento. Semjoin(), a coroutine pode continuar executando por um tempo. UsecancelAndJoin().Ignorar a cooperatividade do cancelamento: loops de CPU sem verificação de
isActiveou pontos de suspensao não serao cancelados.Usar Job() como pai sem entender a propagacao: um Job criado manualmente com
Job()segue as regras normais de propagacao de erro. Para isolamento, useSupervisorJob().Esquecer o tratamento de CancellationException: quando um job e cancelado,
CancellationExceptione lancada. Ela deve ser propagada, não capturada emcatchgenerico.Nao estruturar coroutines: lancar coroutines com
GlobalScopeem vez de usar escopos estruturados perde todo o beneficio de hierarquia de Jobs.
Termos relacionados
- Coroutine: a unidade de execução cujo ciclo de vida o Job representa.
- Deferred: subtipo de Job que carrega um resultado, retornado por
async. - SupervisorJob: job especial onde falha de filhos não propaga para irmaos.
- CoroutineScope: escopo que contém um Job e define o ciclo de vida das coroutines.
- Dispatcher: trabalha junto com o Job no contexto da coroutine, determinando onde ela executa.
- Structured Concurrency: o princípio de que coroutines formam hierarquias através de Jobs.
O Job e a peça central do controle de ciclo de vida em Kotlin Coroutines. Dominar Jobs, cancelamento e hierarquia e fundamental para escrever código concorrente que seja robusto, previsivel e livre de vazamentos de recursos.