Padrões de sincronização em segundo plano com WorkManager (Kotlin) para apps de campo
Padrões de sincronização em segundo plano com WorkManager (Kotlin) para apps de campo: escolha o tipo de trabalho certo, defina restrições, use backoff exponencial e mostre progresso visível ao usuário.

O que sincronização em segundo plano confiável significa para apps de campo e ops
Em apps de campo e ops, sincronizar não é um “bom de ter”. É como o trabalho sai do dispositivo e vira real para a equipe. Quando a sincronização falha, os usuários percebem rápido: um serviço concluído continua aparecendo como “pendente”, fotos desaparecem ou o mesmo relatório sobe duas vezes e cria duplicatas.
Esses apps são mais difíceis que aplicativos de consumidor porque os telefones operam nas piores condições. A rede alterna entre LTE, Wi‑Fi fraco e sem sinal. Economizador de bateria bloqueia trabalho em segundo plano. O app é finalizado, o SO atualiza e dispositivos reiniciam no meio da jornada. Uma configuração confiável do WorkManager precisa sobreviver a tudo isso sem drama.
Confiável normalmente significa quatro coisas:
- Eventual consistência: os dados podem chegar atrasados, mas chegam sem precisar de supervisão manual.
- Recuperável: se o app morrer no meio do upload, a próxima execução continua com segurança.
- Observável: usuários e suporte podem ver o que está acontecendo e o que está preso.
- Não destrutivo: retries não criam duplicatas nem corrompem o estado.
“Executar agora” serve para ações pequenas e iniciadas pelo usuário que devem terminar logo (por exemplo, enviar um único status antes do usuário fechar um serviço). “Aguardar” serve para trabalhos maiores como uploads de fotos, atualizações em lote ou qualquer coisa que possa drenar bateria ou falhar em redes ruins.
Exemplo: um inspetor envia um formulário com 12 fotos em um porão sem sinal. Uma sincronização confiável salva tudo localmente, marca como enfileirado e faz upload depois quando o dispositivo tiver conexão real, sem que o inspetor refaça o trabalho.
Escolha os blocos do WorkManager certos
Comece escolhendo a menor e mais clara unidade de trabalho. Essa decisão afeta a confiabilidade mais do que qualquer lógica de retry esperta que você adicione depois.
One-time vs periodic work
Use OneTimeWorkRequest para trabalho que deve acontecer porque algo mudou: um novo formulário foi salvo, uma foto terminou de compactar ou o usuário tocou em Sincronizar. Enfileire imediatamente (com restrições) e deixe o WorkManager rodar quando o dispositivo estiver pronto.
Use PeriodicWorkRequest para manutenção constante, como um “verificar atualizações” ou limpeza noturna. Trabalho periódico não é exato. Tem um intervalo mínimo e pode variar com base nas regras de bateria e do sistema, então não deve ser sua única via para uploads importantes.
Um padrão prático é work one-time para “deve sincronizar em breve”, com trabalho periódico como rede de segurança.
Escolhendo Worker, CoroutineWorker ou RxWorker
Se você escreve em Kotlin e usa funções suspend, prefira CoroutineWorker. Mantém o código curto e faz o cancelamento se comportar como esperado.
Worker encaixa para código bloqueante simples, mas cuidado para não bloquear por muito tempo.
RxWorker faz sentido apenas se seu app já usa RxJava em grande escala. Caso contrário, é complexidade extra.
Encadear etapas ou rodar um worker com fases?
Encadear é ótimo quando as etapas podem falhar ou ter sucesso de forma independente e você quer retries separados e logs mais claros. Um worker com fases pode ser melhor quando as etapas compartilham dados e precisam ser tratadas como uma transação.
Uma regra simples:
- Encadeie quando as etapas tiverem restrições diferentes (upload só em Wi‑Fi, depois uma chamada de API leve).
- Use um worker quando precisar de uma sincronização “tudo ou nada”.
O WorkManager garante que trabalho é persistido, pode sobreviver à morte do processo e reinicializações, e respeita restrições. Não garante tempo exato, execução imediata ou rodar depois que o usuário finalizar o app forçadamente. Se você está construindo um app Android de campo (incluindo um gerado como Kotlin a partir do AppMaster), projete a sincronização para que atrasos sejam seguros e esperados.
Faça a sincronização segura: idempotente, incremental e retomável
Um app de campo vai reexecutar trabalho. Telefones perdem sinal, o SO mata processos e usuários tocam em sincronizar duas vezes porque nada parece ter acontecido. Se sua sincronização em segundo plano não for segura para repetir, você terá registros duplicados, atualizações faltantes ou retries infinitos.
Comece tornando cada chamada ao servidor segura para rodar duas vezes. A abordagem mais simples é uma chave de idempotência por item (por exemplo, um UUID salvo com o registro local) que o servidor trata como “mesma requisição, mesmo resultado”. Se não puder mudar o servidor, use uma chave natural estável e um endpoint de upsert, ou inclua um número de versão para que o servidor rejeite atualizações obsoletas.
Rastreie o estado local explicitamente para que o worker possa retomar depois de um crash sem adivinhar. Uma máquina de estados simples costuma ser suficiente:
- queued
- uploading
- uploaded
- needs-review
- failed-temporary
Mantenha a sincronização incremental. Em vez de “sincronizar tudo”, armazene um cursor como lastSuccessfulTimestamp ou um token emitido pelo servidor. Leia uma pequena página de mudanças, aplique e avance o cursor apenas depois que o lote for totalmente gravado localmente. Lotes pequenos (como 20–100 itens) reduzem timeouts, tornam o progresso visível e limitam quanto trabalho você repete após uma interrupção.
Torne uploads retomáveis também. Para fotos ou payloads grandes, persista o URI do arquivo e metadados do upload, e marque como enviado apenas depois da confirmação do servidor. Se o worker reiniciar, ele continua a partir do último estado conhecido em vez de recomeçar do zero.
Exemplo: um técnico preenche 12 formulários e anexa 8 fotos embaixo terra. Quando o dispositivo reconectar, o worker sobe em lotes, cada formulário tem uma chave de idempotência e o cursor de sincronização avança apenas depois de cada lote bem-sucedido. Se o app for encerrado no meio, rerun do worker termina os itens restantes enfileirados sem duplicar nada.
Restrições que combinam com condições reais dos dispositivos
As restrições são guardrails que impedem a sincronização de drenar baterias, gastar planos de dados ou falhar no pior momento. Você quer restrições que reflitam como os dispositivos se comportam no campo, não como se comportam na sua mesa.
Comece com um conjunto pequeno que proteja usuários mas ainda permita o job rodar na maioria dos dias. Um baseline prático é: exigir conexão de rede, evitar rodar com bateria baixa e evitar quando o armazenamento está criticamente baixo. Adicione “carregando” apenas se o trabalho for pesado e não urgente, porque muitos dispositivos de campo raramente são plugados durante o turno.
Exigir demais é uma razão comum para relatos de “sincronização nunca roda”. Se você exigir Wi‑Fi não medido, carregando e bateria não baixa, basicamente pediu por um momento perfeito que pode nunca acontecer. Se o negócio precisa dos dados hoje, é melhor rodar trabalhos menores com mais frequência do que esperar por condições ideais.
Portais cativos são outro problema real: o telefone diz que está conectado, mas o usuário precisa tocar “Aceitar” numa página de hotel ou Wi‑Fi público. O WorkManager não detecta isso de forma confiável. Trate como falha normal: tente a sincronização, faça timeout rápido e tente depois. Também mantenha uma mensagem simples no app como “Conectado ao Wi‑Fi mas sem acesso à internet” quando puder detectar isso durante a requisição.
Use restrições diferentes para uploads pequenos vs grandes para manter o app responsivo:
- Payloads pequenos (pings de status, metadados de formulário): qualquer rede, bateria não baixa.
- Payloads grandes (fotos, vídeos, pacotes de mapa): rede não medida quando possível, e considere exigir carregamento.
Exemplo: um técnico salva um formulário com 2 fotos. Envie os campos do formulário em qualquer conexão, mas enfileire os uploads das fotos para Wi‑Fi ou um momento melhor. O escritório vê o trabalho rápido, e o dispositivo não devora dados móveis enviando imagens em segundo plano.
Retries com backoff exponencial que não irritam os usuários
Retries é onde apps de campo ou parecem calmos ou parecem quebrados. Escolha uma política de backoff que combine com o tipo de falha que você espera.
Backoff exponencial é geralmente o padrão mais seguro para redes. Ele aumenta rapidamente o tempo de espera para não atormentar o servidor nem drenar bateria quando a cobertura está ruim. Backoff linear pode servir para problemas temporários curtos (por exemplo, uma VPN instável), mas tende a tentar com muita frequência em áreas de sinal fraco.
Baseie decisões de retry no tipo de falha, não só em “algo falhou”. Um conjunto de regras simples ajuda:
- Timeout de rede, 5xx, DNS, sem conectividade:
Result.retry() - Auth expirado (401): renove token uma vez, depois falhe e peça para usuário fazer login
- Validação ou 4xx (bad request):
Result.failure()com erro claro para suporte - Conflito (409) para itens já enviados: trate como sucesso se sua sincronização for idempotente
Limite o dano para que um erro permanente não entre em loop infinito. Defina um número máximo de tentativas e, depois disso, pare e mostre uma mensagem discreta e acionável (não notificações repetidas).
Você também pode mudar o comportamento conforme as tentativas aumentam. Por exemplo, depois de 2 falhas, envie lotes menores ou pule uploads grandes até o próximo pull bem-sucedido.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.build()
// in doWork()
if (runAttemptCount >= 5) return Result.failure()
return Result.retry()
Isso mantém os retries educados: menos wakeups, menos interrupções aos usuários e recuperação mais rápida quando a conexão finalmente volta.
Progresso visível ao usuário: notificações, trabalho em foreground e status
Apps de campo muitas vezes sincronizam quando o usuário menos espera: num porão, em rede lenta, com bateria quase no fim. Se a sincronização afeta o que o usuário está esperando (uploads, envio de relatórios, lotes de fotos), torne isso visível e fácil de entender. Trabalho silencioso em background é ótimo para atualizações pequenas e rápidas. Qualquer coisa mais longa deve ser honesta.
Quando execução em foreground é necessária
Use execução foreground quando um job for de longa duração, sensível ao tempo ou claramente ligado a uma ação do usuário. No Android moderno, uploads grandes podem ser parados ou adiados a menos que você rode em foreground. No WorkManager, isso significa retornar um ForegroundInfo para que o sistema mostre uma notificação em andamento.
Uma boa notificação responde três perguntas: o que está sincronizando, o quão avançado está e como parar. Adicione uma ação clara de cancelar para que o usuário possa desistir se estiver em dados medidos ou precisar do telefone no momento.
Progresso em que as pessoas confiam
Progresso deve mapear para unidades reais, não porcentagens vagas. Atualize o progresso com setProgress e leia de WorkInfo na sua UI (ou numa tela de status).
Se você está enviando 12 fotos e 3 formulários, reporte “5 de 15 itens enviados”, mostre o que resta e mantenha a última mensagem de erro para suporte.
Mantenha o progresso significativo:
- Itens feitos e itens restantes
- Etapa atual ("Enviando fotos", "Enviando formulários", "Finalizando")
- Último horário de sincronização bem-sucedida
- Último erro (curto, amigável ao usuário)
- Uma opção visível de cancelar/parar
Se sua equipe constrói ferramentas internas rapidamente com AppMaster, mantenha a mesma regra: usuários confiam na sincronização quando podem vê‑la e quando ela corresponde ao que estão tentando fazer.
Trabalho único, tags e evitando jobs de sincronização duplicados
Jobs de sincronização duplicados são uma das maneiras mais fáceis de drenar bateria, gastar dados móveis e criar conflitos no servidor. WorkManager oferece duas ferramentas simples para evitar isso: nomes de trabalho únicos e tags.
Um bom padrão é tratar “sincronização” como uma única pista. Em vez de enfileirar um novo job toda vez que o app acorda, enfileire com o mesmo nome de trabalho único. Assim, você não terá uma tempestade de sync quando o usuário abrir o app, a mudança de rede disparar e um job periódico também rodar.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag("sync")
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)
Escolher a política é a principal escolha de comportamento:
KEEP: se já houver uma sincronização rodando (ou enfileirada), ignore a nova requisição. Use isso para a maioria dos botões “Sincronizar agora” e gatilhos automáticos.REPLACE: cancele a atual e comece do zero. Use quando os inputs realmente mudaram, como o usuário trocou de conta ou projeto.
Tags são seu ponto de controle e visibilidade. Com uma tag estável como sync, você pode cancelar, consultar status ou filtrar logs sem rastrear IDs específicos. Isso é útil para uma ação manual “sincronizar agora”: você pode checar se já há trabalho em execução e mostrar uma mensagem clara em vez de lançar outro worker.
Sincronização periódica e on‑demand não devem brigar entre si. Mantenha separadas, mas coordenadas:
- Use
enqueueUniquePeriodicWork("sync_periodic", KEEP, ...)para o job agendado. - Use
enqueueUniqueWork("sync", KEEP, ...)para demanda. - No seu worker, saia rápido se não houver nada para enviar ou baixar, para que a execução periódica seja barata.
- Opcionalmente, faça o worker periódico enfileirar o mesmo trabalho one‑time único, para que todo trabalho real aconteça num só lugar.
Esses padrões mantêm a sincronização previsível: uma sincronização por vez, fácil de cancelar e fácil de observar.
Passo a passo: um pipeline prático de sincronização em segundo plano
Um pipeline confiável fica mais fácil quando você o trata como uma pequena máquina de estados: itens de trabalho vivem localmente primeiro, e o WorkManager só os move adiante quando as condições são certas.
Um pipeline simples que você pode entregar
-
Comece com tabelas locais de “fila”. Armazene os menores metadados necessários para retomar: id do item, tipo (formulário, foto, nota), status (pending, uploading, done), contador de tentativas, último erro e um cursor ou revisão do servidor para downloads.
-
Para um “Sincronizar agora” acionado pelo usuário, enfileire um
OneTimeWorkRequestcom restrições que batam com o mundo real. Escolhas comuns são rede conectada e bateria não baixa. Se uploads forem pesados, também exija carregamento. -
Implemente um
CoroutineWorkercom fases claras: upload, download, reconciliar. Mantenha cada fase incremental. Faça upload apenas de itens marcados como pending, baixe apenas mudanças desde seu último cursor e depois reconcilie conflitos com regras simples (por exemplo: servidor vence para campos de atribuição, cliente vence para rascunhos locais). -
Adicione retries com backoff, mas seja seletivo sobre o que você retenta. Timeouts e 500s devem ser retentados. Um 401 (deslogado) deve falhar rápido e informar a UI o que aconteceu.
-
Observe
WorkInfopara guiar UI e notificações. Use updates de progresso para fases como “Enviando 3 de 10” e mostre uma mensagem curta de falha que indique a próxima ação (tentar de novo, fazer login, conectar ao Wi‑Fi).
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
Quando você mantém a fila local e as fases do worker explícitas, obtém um comportamento previsível: o trabalho pode pausar, retomar e explicar-se ao usuário sem adivinhar o que aconteceu.
Erros comuns e armadilhas (e como evitá‑los)
Sincronização confiável falha mais frequentemente por algumas escolhas pequenas que parecem inofensivas em testes, mas desmoronam em dispositivos reais. O objetivo não é rodar o máximo possível. É rodar no momento certo, fazer o trabalho certo e parar limpo quando não for possível.
Armadilhas para ficar de olho
- Fazer uploads grandes sem restrições. Se você enviar fotos ou payloads grandes em qualquer rede e qualquer nível de bateria, os usuários sentirão. Adicione restrições para tipo de rede e bateria baixa, e divida trabalho grande em pedaços menores.
- Retentar todo erro para sempre. Um 401, token expirado ou permissão faltando não é problema temporário. Marque como falha crítica, expose uma ação clara (relogin) e retente apenas problemas transitórios como timeouts.
- Criar duplicatas por acidente. Se um worker pode rodar duas vezes, o servidor verá criações duplas a menos que as requisições sejam idempotentes. Use ID gerado pelo cliente por item e faça o servidor tratar repetições como updates, não novos registros.
- Usar trabalho periódico para necessidades quase em tempo real. Trabalho periódico é melhor para manutenção, não para “sincronize agora”. Para sincronização iniciada pelo usuário, enfileire trabalho one‑time único e permita que o usuário o dispare quando necessário.
- Reportar “100%” cedo demais. Conclusão de upload não é a mesma coisa que dado aceito e reconciliado. Rastreie progresso por estágios (queued, uploading, server confirmed) e só mostre concluído após confirmação.
Um exemplo concreto: um técnico submete um formulário com três fotos num elevador com sinal fraco. Se você começar imediatamente sem restrições, uploads travam, retries disparam e o formulário pode ser criado duas vezes quando o app reiniciar. Se você restringir a uma rede utilizável, subir em etapas e dar a cada formulário uma ID estável, o mesmo cenário termina com um único registro no servidor e uma mensagem de progresso verdadeira.
Checklist rápido antes de enviar
Antes de lançar, teste a sincronização do jeito que usuários de campo reais vão quebrá‑la: sinal intermitente, baterias mortas e muito toque na tela. O que parece ok em um dev phone pode falhar no mundo real se agendamento, retries ou report de status estiverem errados.
Execute esses testes em pelo menos um dispositivo lento e um mais novo. Mantenha logs, mas também veja o que o usuário vê na UI.
- No network e depois recuperação: Inicie uma sincronização com conectividade desligada, depois ligue. Confirme que o trabalho é enfileirado (não falha rápido) e retoma depois sem duplicar uploads.
- Reinicialização do dispositivo: Comece uma sincronização, reinicie no meio e depois abra o app. Verifique se o trabalho continua ou é re‑agendado corretamente, e que o app mostra o estado certo (não preso em "sincronizando").
- Bateria baixa e armazenamento cheio: Ative o economizador, caia abaixo do limiar de bateria baixa se possível e encha o armazenamento quase ao máximo. Confirme que o job espera quando deve e continua quando as condições melhoram, sem drenar bateria em loop de retries.
- Gatilhos repetidos: Toque o botão “Sincronizar” várias vezes ou dispare sync de várias telas. Você ainda deve terminar com uma única execução lógica, não uma pilha de workers paralelos competindo pelos mesmos registros.
- Falhas no servidor que você pode explicar: Simule 500s, timeouts e erros de auth. Verifique se os retries recuam e param após um limite, e que o usuário vê uma mensagem clara como "Não foi possível alcançar o servidor, tentaremos novamente" em vez de uma falha genérica.
Se qualquer teste deixar o app em estado confuso, trate isso como bug. Usuários perdoam sincronização lenta, mas não perdoam perda de dados ou não saber o que aconteceu.
Cenário de exemplo: formulários offline e uploads de fotos em um app de campo
Um técnico chega a um local com cobertura fraca. Ele preenche um formulário offline, captura 12 fotos e toca em Enviar antes de sair. O app salva tudo localmente primeiro (por exemplo, em um banco local): um registro para o formulário e um por foto com um estado claro como PENDING, UPLOADING, DONE ou FAILED.
Ao tocar em Enviar, o app enfileira um job de sincronização único para não criar duplicatas se tocar duas vezes. Um setup comum é uma cadeia WorkManager que envia fotos primeiro (maiores e mais lentas) e depois envia o payload do formulário após os anexos serem confirmados.
A sincronização só roda quando as condições batem com o mundo real. Por exemplo, espera por rede conectada, bateria não baixa e armazenamento suficiente. Se o técnico ainda estiver no porão sem sinal, nada devora a bateria em loops de background.
O progresso é óbvio e amigável. O upload roda como trabalho foreground e mostra uma notificação tipo “Enviando 3 de 12”, com um Cancelar claro. Se cancelar, o app para o trabalho e mantém os itens restantes em PENDING para que possam tentar novamente depois sem perda de dados.
Retries se comportam de forma educada após um hotspot instável: a primeira falha tenta de novo logo, mas cada falha espera mais tempo (backoff exponencial). Parece responsivo no começo e depois desacelera para evitar drenar bateria e spam na rede.
Para a equipe de ops, o ganho é prático: menos envios duplicados porque itens são idempotentes e enfileirados unicamente, estados de falha claros (qual foto falhou, por quê e quando vai tentar de novo) e mais confiança de que “enviado” significa “armazenado com segurança e será sincronizado”.
Próximos passos: entregue confiabilidade primeiro, depois expanda o escopo da sincronização
Antes de adicionar mais recursos de sync, deixe claro o que significa “feito”. Para a maioria dos apps de campo, não é “requisição enviada”. É “servidor aceitou e confirmou”, mais um estado de UI que corresponde à realidade. Um formulário que diz “Sincronizado” deve permanecer assim após reiniciar o app, e um formulário que falhou deve mostrar o que fazer em seguida.
Torne o app fácil de confiar adicionando um pequeno conjunto de sinais que as pessoas possam ver (e que o suporte possa perguntar). Mantenha-os simples e consistentes entre telas:
- Última sincronização bem‑sucedida
- Último erro de sincronização (mensagem curta, não stack trace)
- Itens pendentes (por exemplo: 3 formulários, 12 fotos)
- Estado atual da sincronização (Idle, Syncing, Needs attention)
Trate observabilidade como parte do recurso. Isso economiza horas em campo quando alguém está em conexão fraca e não sabe se o app está funcionando.
Se você também está construindo backend e ferramentas admin, gerá‑los juntos ajuda a manter o contrato de sincronização estável. AppMaster (appmaster.io) pode gerar um backend pronto para produção, um painel admin web e apps móveis nativos, o que ajuda a manter modelos e auth alinhados enquanto você foca nas bordas difíceis da sincronização.
Por fim, rode um pequeno piloto. Escolha uma fatia de ponta a ponta (por exemplo, “enviar formulário de inspeção com 1–2 fotos”) e entregue com restrições, retries e progresso visível funcionando totalmente. Quando essa fatia virar rotina previsível, expanda uma funcionalidade por vez.
FAQ
Sincronização confiável significa que o trabalho criado no dispositivo é salvo localmente primeiro e será enviado depois sem que o usuário precise refazer os passos. Deve sobreviver a encerramentos do app, reinicializações, redes fracas e tentativas repetidas sem perder dados ou criar duplicatas.
Use trabalho one-time para qualquer coisa acionada por um evento real, como “formulário salvo”, “foto adicionada” ou o usuário apertar Sincronizar. Use trabalho periódico para manutenção e como rede de segurança, mas não como a única via para uploads importantes, pois a temporização pode variar.
Se você usa Kotlin e suas chamadas usam suspend, CoroutineWorker é a escolha mais simples e previsível, especialmente para cancelamento. Use Worker só para tarefas curtas e bloqueantes, e RxWorker apenas se o app já estiver fortemente baseado em RxJava.
Encadeie workers quando as etapas tiverem restrições diferentes ou precisarem de retries independentes (por exemplo, upload grande em Wi‑Fi e depois uma chamada leve de API). Use um único worker com fases claras quando as etapas compartilharem estado e você quiser comportamento “tudo ou nada” para uma sincronização lógica.
Torne cada requisição de criação/atualização segura para ser executada duas vezes usando uma chave de idempotência por item (por exemplo, um UUID salvo no registro local). Se não puder alterar o servidor, prefira upserts com chaves estáveis ou cheques de versão para que repetições não criem novos registros.
Persista estados locais explícitos como queued, uploading, uploaded e failed para que o worker possa retomar sem adivinhações. Só marque um item como concluído após o servidor confirmar, e armazene metadados suficientes (URI do arquivo, contagem de tentativas) para continuar após um crash ou reboot.
Comece com restrições mínimas que protejam os usuários mas permitam que a sincronização rode na maioria dos dias: exigir conexão de rede, evitar rodar com bateria baixa e evitar quando o armazenamento está crítico. Tenha cuidado com “unmetered” e “charging”, pois podem fazer a sincronização praticamente nunca ocorrer em dispositivos de campo.
Trate “conectado mas sem internet” como uma falha normal: faça timeout rápido, retorne Result.retry() e tente novamente depois. Se conseguir detectar isso durante a requisição, mostre uma mensagem simples para o usuário entender por que o dispositivo parece online mas a sincronização não avança.
Use backoff exponencial para falhas de rede para que as tentativas fiquem menos frequentes quando a cobertura estiver ruim. Retente timeouts e erros 5xx, falhe rápido em problemas permanentes como requisições inválidas, e limite o número de tentativas para não ficar em loop quando o usuário precisa agir (por exemplo, fazer login novamente).
Enfileire a sincronização como trabalho único (unique work) para que múltiplos gatilhos não iniciem jobs paralelos, e exponha progresso real para uploads longos. Se o trabalho for demorado ou iniciado pelo usuário, rode em foreground com uma notificação contínua que mostre contagens reais e ofereça uma opção clara de cancelar.


