WorkManager é uma das bibliotecas mais importantes do Android moderno para quem precisa executar trabalho em segundo plano de forma confiável. Em apps reais, nem tudo deve acontecer enquanto a tela está aberta: sincronizar dados pendentes, enviar logs, fazer upload de arquivos, baixar conteúdo para uso offline, limpar cache antigo e reagendar tarefas depois de uma falha são necessidades comuns. Com Kotlin no Android, o caminho idiomático é combinar WorkManager, coroutines, constraints e uma arquitetura clara de repository.
O erro comum é tratar WorkManager como “uma coroutine que roda depois”. Ele não é isso. WorkManager é um agendador persistente para trabalho adiado e garantido, respeitando condições do sistema como rede, bateria, armazenamento, reinício do aparelho e políticas do Android para background execution. Quando usado bem, ele deixa o app mais resiliente. Quando usado mal, vira uma fonte de workers duplicados, retries infinitos e consumo de bateria.
Este guia mostra quando usar WorkManager, como criar um CoroutineWorker, como configurar restrições, retry, dados de entrada, execução única, testes e observabilidade. A ideia é complementar temas que aparecem em Android offline-first com Kotlin, Room, Flow e arquitetura Android para vagas em 2026.
Quando usar WorkManager?
Use WorkManager quando o trabalho precisa continuar sendo importante mesmo se o usuário sair da tela, fechar o app ou reiniciar o aparelho. Alguns exemplos clássicos:
- sincronizar operações criadas offline;
- enviar fotos ou anexos quando houver Wi-Fi;
- atualizar dados de catálogo em background;
- enviar eventos acumulados de analytics interno;
- limpar arquivos temporários antigos;
- baixar conteúdo para leitura offline;
- fazer retry de uma chamada que falhou por rede instável.
Não use WorkManager para tudo. Se a ação precisa responder imediatamente ao clique do usuário, execute no ViewModel ou no repository com coroutines. Se é uma tarefa muito curta ligada ao ciclo de vida da tela, WorkManager adiciona complexidade desnecessária. Se precisa rodar exatamente em um horário preciso, talvez AlarmManager faça mais sentido. Se é streaming contínuo, use um foreground service bem justificado.
Uma regra prática: WorkManager é ideal para trabalho adiável, persistente e com garantia razoável de execução. Ele não promete execução instantânea. O sistema decide o melhor momento considerando bateria, rede e outras restrições.
Dependências básicas
Em um projeto Android com Gradle Kotlin DSL, você normalmente adiciona a dependência do runtime Kotlin:
dependencies {
implementation("androidx.work:work-runtime-ktx:2.10.0")
}
Confira sempre a versão mais recente compatível com o seu projeto. Em apps com injeção de dependência, você também pode precisar de integração com Hilt ou uma WorkerFactory customizada.
Criando um CoroutineWorker
Para Kotlin, prefira CoroutineWorker em vez de bloquear uma thread manualmente. O método doWork() é suspend, então você pode chamar repositories, DAOs e APIs que já usam coroutines.
class SincronizarFavoritosWorker(
appContext: Context,
params: WorkerParameters,
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val repository = ServiceLocator.favoritosRepository(applicationContext)
return try {
repository.sincronizarFavoritosPendentes()
Result.success()
} catch (e: IOException) {
Result.retry()
} catch (e: HttpException) {
if (e.code() in 500..599) Result.retry() else Result.failure()
}
}
}
O exemplo usa ServiceLocator apenas para manter o código curto. Em produção, prefira injeção explícita com Hilt, Koin ou uma WorkerFactory. O worker não deve conter regra de negócio complexa. Ele deve coordenar a execução e delegar a lógica para um repository ou use case testável.
Entendendo success, retry e failure
O retorno do worker comunica ao WorkManager o que aconteceu:
Result.success()indica que o trabalho terminou;Result.retry()pede nova tentativa com política de backoff;Result.failure()encerra a cadeia como falha definitiva.
Não retorne retry() para qualquer erro. Falha de rede, timeout e erro 5xx costumam ser retryable. Erro 401, payload inválido ou regra de negócio rejeitada normalmente precisam de correção local, logout ou marcação da operação como falha definitiva. Retry infinito para erro permanente só gasta bateria e esconde o problema.
Em apps offline-first, o repository pode guardar o status da operação pendente no Room. Assim, mesmo quando o worker retorna failure(), a UI consegue explicar o estado ao usuário: “Não foi possível sincronizar esta alteração. Toque para tentar novamente.”
Agendando trabalho com constraints
Constraints evitam executar trabalho no momento errado. Se a sincronização depende de internet, declare isso. Se o upload é grande, talvez exija Wi-Fi ou bateria não baixa.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<SincronizarFavoritosWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30.seconds.toJavaDuration(),
)
.addTag("sync-favoritos")
.build()
WorkManager.getInstance(context).enqueue(request)
Use constraints com intenção. Exigir Wi-Fi para uma sincronização pequena pode atrasar o app sem necessidade. Não exigir rede para um worker que só chama API gera falhas previsíveis. A melhor configuração depende do impacto no usuário, tamanho dos dados e urgência da operação.
Evitando workers duplicados
Um dos problemas mais comuns é enfileirar o mesmo trabalho várias vezes. O usuário favoritou cinco itens? O app voltou da tela de login? A conectividade mudou? Sem cuidado, cada evento dispara um worker novo.
Para sincronização de fila, geralmente faz sentido usar enqueueUniqueWork:
WorkManager.getInstance(context).enqueueUniqueWork(
"sync-favoritos",
ExistingWorkPolicy.KEEP,
request,
)
KEEP preserva um trabalho já pendente ou em execução. É uma boa opção quando “uma sincronização em andamento” já é suficiente. REPLACE cancela a anterior e agenda outra; use quando a nova solicitação realmente torna a antiga obsoleta. APPEND entra em cena quando você precisa encadear etapas, mas não deveria ser a primeira escolha para sync simples.
O nome do trabalho único deve ser estável e específico. sync é amplo demais. sync-favoritos, upload-avatar ou limpeza-cache-noticias comunicam melhor a intenção.
Passando dados de entrada
Workers podem receber dados pequenos por Data. Isso é útil para IDs, flags e parâmetros simples.
val input = workDataOf(
"usuarioId" to usuarioId,
"forcarRefresh" to true,
)
val request = OneTimeWorkRequestBuilder<SincronizarPerfilWorker>()
.setInputData(input)
.build()
Dentro do worker:
override suspend fun doWork(): Result {
val usuarioId = inputData.getString("usuarioId") ?: return Result.failure()
val forcarRefresh = inputData.getBoolean("forcarRefresh", false)
repository.sincronizarPerfil(usuarioId, forcarRefresh)
return Result.success()
}
Não coloque payload grande em Data. Se você precisa sincronizar muitas operações, grave a fila no Room e passe apenas um identificador ou nem passe nada. O worker lê o banco e decide o que está pendente.
Trabalho periódico: use com cuidado
WorkManager também suporta tarefas periódicas, mas elas não são cron de servidor. O Android pode atrasar a execução para preservar bateria. Use periodic work para manutenção eventual, não para lógica que precisa rodar em minuto exato.
val request = PeriodicWorkRequestBuilder<AtualizarCatalogoWorker>(
repeatInterval = 6.hours,
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.build(),
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"atualizar-catalogo",
ExistingPeriodicWorkPolicy.KEEP,
request,
)
Para dados que o usuário precisa ver imediatamente, combine periodic work com refresh manual ou refresh ao abrir a tela. O trabalho periódico mantém o app razoavelmente atualizado; ele não substitui uma boa estratégia de estado local e invalidação.
WorkManager em arquitetura offline-first
Em apps offline-first, WorkManager deve ser uma peça da arquitetura, não um atalho que acessa tudo diretamente. Um desenho saudável fica assim:
- A UI envia uma intenção para o ViewModel.
- O repository grava a alteração local no Room.
- O repository marca a operação como pendente.
- O app agenda um worker único de sync.
- O worker chama o repository para processar pendências.
- O repository atualiza Room com sucesso, erro ou conflito.
- A UI observa o estado local via Flow.
Essa separação mantém a tela rápida e previsível. O usuário vê a ação aplicada localmente, enquanto a sincronização acontece no momento apropriado. Se a API falha, o estado pendente continua visível e testável.
Observando o estado do trabalho
Você pode observar o estado de um work pelo ID ou por tag:
val workInfos: Flow<List<WorkInfo>> =
WorkManager.getInstance(context)
.getWorkInfosByTagFlow("sync-favoritos")
Esse estado é útil para debug e alguns indicadores de UI, mas não deveria ser a única fonte de verdade do produto. Para uma fila offline-first, o Room continua sendo mais confiável para responder “quantas operações estão pendentes?” ou “qual item falhou?”. WorkInfo explica a execução do worker; o banco explica o estado de negócio.
Testando workers
Teste a regra principal no repository com testes unitários. O worker deve ter pouco código, mas ainda vale validar retorno success, retry e failure quando possível. O WorkManager tem artefatos de teste para inicializar o ambiente em testes instrumentados.
O que vale cobrir:
- erro de rede retorna retry;
- erro permanente retorna failure;
- sucesso chama o repository e limpa pendências;
- workers duplicados não são criados para o mesmo fluxo;
- constraints estão configuradas para rede quando necessário;
- a UI mostra estado pendente mesmo se o worker ainda não rodou.
Se o app já usa JUnit e MockK, deixe a maior parte da complexidade fora do worker. Mockar WorkManager demais costuma deixar o teste frágil; testar o repository e fazer um smoke test do agendamento geralmente traz melhor retorno.
Erros comuns com WorkManager
O primeiro erro é transformar worker em “classe Deus”. Worker não deve montar payload, decidir regra de negócio, manipular UI indireta e conhecer detalhes de API ao mesmo tempo. Delegue.
O segundo erro é ignorar idempotência. Um worker pode rodar mais de uma vez depois de retry. Se enviar a mesma operação duas vezes quebra o backend, a operação precisa de identificador idempotente ou controle de status local.
O terceiro erro é usar periodic work para tentar simular push notification. Se o servidor precisa avisar o app, use FCM ou outra estratégia apropriada. Periodic work atrasado por bateria não é canal de tempo real.
O quarto erro é não registrar nada. Em produção, falhas de sync precisam de logs estruturados, contadores e, em casos críticos, alertas. Para aprofundar esse lado, veja também observabilidade em Kotlin e CI/CD para Kotlin.
Checklist prático
Antes de colocar WorkManager em produção, valide:
- o trabalho é realmente adiável e persistente;
- há constraints de rede/bateria coerentes;
- retries diferenciam erro temporário e permanente;
- o worker é idempotente;
enqueueUniqueWorkevita duplicação quando necessário;- dados grandes ficam no Room, não no
Data; - a UI não depende apenas do WorkInfo para estado de negócio;
- logs e métricas permitem investigar falhas.
Conclusão
WorkManager com Kotlin é essencial para apps Android que precisam ser confiáveis fora do fluxo imediato da tela. Ele resolve execução adiada, retry e sobrevivência a reinício do app, mas não substitui arquitetura. A combinação mais forte em 2026 é usar CoroutineWorker como orquestrador fino, repositories para regra de negócio, Room para estado local, Flow para UI reativa e constraints para respeitar o dispositivo do usuário.
Para quem está estudando Android ou montando portfólio, um projeto que usa WorkManager corretamente demonstra maturidade: você entende rede instável, bateria, sincronização, retries e experiência real de produto. Esse é exatamente o tipo de detalhe que diferencia um app de tutorial de um app pronto para produção.