Sincronização em segundo plano (offline-first) para apps móveis: conflitos, reenvios e UX
Planeje sincronização em segundo plano offline-first para apps móveis com regras claras de conflito, lógica de reenvio e uma UX simples de alterações pendentes para apps nativos Kotlin e SwiftUI.

O problema: usuários editam offline e a realidade muda
Alguém começa uma tarefa com boa conexão, depois entra em um elevador, um canto de armazém ou um túnel de metrô. O app continua funcionando, então a pessoa continua trabalhando. Ela toca em Salvar, adiciona uma nota, muda um status, talvez até cria um novo registro. Tudo parece certo porque a tela atualiza na hora.
Depois, a conexão volta e o app tenta sincronizar em segundo plano. É aí que a sincronização em background pode surpreender.
Se o app não for cuidadoso, a mesma ação pode ser enviada duas vezes (duplicados), ou uma mudança mais recente no servidor pode sobrescrever o que o usuário acabou de fazer (edições perdidas). Às vezes o app mostra estados confusos como “Salvo” e “Não salvo” ao mesmo tempo, ou um registro aparece, some e reaparece depois da sincronização.
Um conflito é simples: duas mudanças diferentes foram feitas na mesma coisa antes que o app pudesse reconciliá-las. Por exemplo, um agente de suporte altera a prioridade de um ticket para Alta enquanto está offline, mas um colega online fecha o ticket. Quando o telefone offline reconecta, ambas as mudanças não podem ser aplicadas limpas sem uma regra.
O objetivo não é fazer o offline parecer perfeito. O objetivo é torná-lo previsível:
- As pessoas conseguem continuar trabalhando sem medo de perder o que fizeram.
- A sincronização acontece depois sem duplicados misteriosos.
- Quando algo precisa de atenção, o app explica claramente o que aconteceu e o que fazer a seguir.
Isso vale tanto se você codar à mão em Kotlin/SwiftUI quanto se construir apps nativos numa plataforma no-code como AppMaster. A parte difícil não são os widgets da UI. É decidir como o app se comporta quando o mundo muda enquanto o usuário está offline.
Um modelo simples offline-first (sem jargão)
Um app offline-first assume que o telefone vai perder rede às vezes, mas o app deve continuar utilizável. As telas devem carregar e os botões devem funcionar mesmo quando o servidor não está acessível.
Quatro termos cobrem a maior parte:
- Cache local: dados armazenados no dispositivo para que o app mostre algo instantaneamente.
- Fila de sincronização: lista de ações que o usuário fez enquanto estava offline (ou com rede instável).
- Verdade do servidor: a versão armazenada no backend que todos compartilham eventualmente.
- Conflito: quando a mudança do usuário na fila não se aplica limpidamente porque a versão do servidor mudou.
Um modelo mental útil é separar leituras de escritas.
Leituras normalmente são diretas: mostre o melhor dado disponível (frequentemente do cache local) e então atualize silenciosamente quando a rede voltar.
Escritas são diferentes. Não confie em “salvar todo o registro” de uma vez. Isso quebra assim que você fica offline.
Em vez disso, registre o que o usuário fez como pequenas entradas em um log de mudanças. Por exemplo: “definir status como Aprovado”, “adicionar comentário X”, “alterar quantidade de 2 para 3”. Cada entrada vai para a fila de sincronização com timestamp e um ID. A sincronização em background tenta então entregá-la.
O usuário continua trabalhando enquanto as mudanças passam de pendentes para sincronizadas.
Se você usa uma plataforma no-code como AppMaster, ainda quer os mesmos blocos: leituras em cache para telas rápidas, e uma fila clara de ações do usuário que podem ser re-tentadas, mescladas ou sinalizadas quando ocorre um conflito.
Decida o que realmente precisa de suporte offline
Offline-first pode soar como “tudo funciona sem conexão”, mas essa promessa é onde muitos apps se complicam. Escolha as partes que realmente se beneficiam de suporte offline e mantenha o resto claramente online-only.
Pense em termos de intenção do usuário: o que as pessoas precisam fazer num porão, num avião ou num armazém com sinal intermitente? Um bom padrão é suportar ações que criam ou atualizam trabalho diário e bloquear ações onde a “verdade mais recente” importa.
Um conjunto prático de ações offline-friendly costuma incluir criação e edição de registros principais (notas, tarefas, inspeções, tickets), rascunhos de comentários e anexar fotos (armazenadas localmente, enviadas depois). Excluir também pode funcionar, mas é mais seguro como exclusão suave com uma janela de desfazer até o servidor confirmar.
Agora decida o que precisa ficar em tempo real porque o risco é alto. Pagamentos, mudanças de permissão, aprovações e qualquer coisa envolvendo dados sensíveis geralmente devem exigir conexão. Se o usuário não pode ter certeza se a ação é válida sem checar o servidor, não permita offline. Mostre uma mensagem clara “requer conexão”, não um erro misterioso.
Defina expectativas sobre atualidade. “Offline” não é binário. Determine quão defasados os dados podem ficar: minutos, horas ou “na próxima vez que o app abrir”. Coloque essa regra na UI em palavras simples, como “Última atualização há 2 horas” e “Sincronizando quando online”.
Por fim, sinalize cedo dados com alto potencial de conflito. Contagens de inventário, tarefas compartilhadas e mensagens de equipe são ímãs de conflito porque várias pessoas editam rapidamente. Para esses, considere limitar edições offline a rascunhos ou capturar mudanças como eventos separados em vez de sobrescrever um único valor.
Se você está construindo no AppMaster, essa etapa de decisão ajuda a modelar dados e regras de negócio para que o app armazene rascunhos seguros offline enquanto mantém ações arriscadas online-only.
Projete a fila de sincronização: o que armazenar para cada mudança
Quando um usuário trabalha offline, não tente “sincronizar o banco inteiro”. Sincronize as ações do usuário. Uma fila clara de ações é a espinha dorsal da sincronização em background e continua compreensível quando algo dá errado.
Mantenha ações pequenas e humanas, alinhadas com o que o usuário realmente fez:
- Criar um registro
- Atualizar campo(s) específicos
- Mudar status (enviar, aprovar, arquivar)
- Excluir (preferencialmente exclusão suave até confirmação)
Ações pequenas são mais fáceis de depurar. Se o suporte precisar ajudar um usuário, fica muito mais simples ler “Mudou status Rascunho -> Enviado” do que inspecionar um grande blob JSON alterado.
Para cada ação enfileirada, armazene metadados suficientes para reproduzi-la com segurança e detectar conflitos:
- Identificador do registro (e um ID local temporário para registros novos)
- Timestamp da ação e identificador do dispositivo
- Versão esperada (ou último updatedAt conhecido) do registro
- Payload (os campos específicos alterados, mais o valor antigo se possível)
- Chave de idempotência (um ID único da ação para que reenvios não criem duplicados)
Essa versão esperada é a chave para um tratamento honesto de conflitos. Se a versão do servidor avançou, você pode pausar e pedir uma decisão em vez de sobrescrever silenciosamente outra pessoa.
Algumas ações precisam ser aplicadas juntas porque o usuário as percebe como um único passo. Por exemplo, “Criar pedido” mais “Adicionar três itens” deve ter sucesso ou falhar como um todo. Armazene um group ID (ou transaction ID) para que o motor de sincronização envie tudo junto e ou confirme todas ou mantenha tudo pendente.
Seja você implementando manualmente ou no AppMaster, o objetivo é o mesmo: cada mudança é registrada uma vez, reproduzida com segurança e explicável quando algo não bate.
Regras de resolução de conflitos que você pode explicar aos usuários
Conflitos são normais. O objetivo não é torná-los impossíveis. É torná-los raros, seguros e fáceis de explicar quando acontecerem.
Nomeie o momento em que o conflito ocorre: o app envia uma mudança e o servidor responde: “Esse registro não é a versão que você começou a editar.” Por isso versionamento importa.
Mantenha dois valores com cada registro:
- Versão do servidor (a versão atual no servidor)
- Versão esperada (a versão que o telefone achava que estava editando)
Se a versão esperada coincidir, aceite a atualização e incremente a versão do servidor. Se não coincidir, aplique sua regra de conflito.
Escolha uma regra por tipo de dado (não uma única regra para tudo)
Dados diferentes precisam de regras diferentes. Um campo de status não é o mesmo que uma nota longa.
Regras que os usuários tendem a entender:
- Última escrita vence: ok para campos de baixo risco, como preferência de visualização.
- Mesclar campos: bom quando campos são independentes (status vs notas).
- Perguntar ao usuário: melhor para edições de alto risco como preço, permissões ou totais.
- Servidor vence com cópia: mantenha o valor do servidor, mas salve a edição do usuário como rascunho que ele pode reaplicar.
No AppMaster, essas regras se mapeiam bem para lógica visual: verifique versões, compare campos e então escolha o caminho.
Decida como deletes se comportam (ou você perderá dados)
Deletes são o caso complicado. Use um tombstone (marcador “deletado”) em vez de remover o registro imediatamente. Depois decida o que acontece se alguém editar um registro que foi deletado em outro lugar.
Uma regra clara é: “Deletes vencem, mas você pode restaurar.” Exemplo: um vendedor edita a nota de um cliente offline, enquanto um administrador deleta esse cliente. Quando a sincronização roda, o app mostra “Cliente foi deletado. Restaurar para aplicar sua nota?” Isso evita perda silenciosa e mantém o controle com o usuário.
Reenvios e estados de falha: mantenha previsível
Quando a sincronização falha, a maioria dos usuários não se importa com o motivo. Eles querem saber se o trabalho está seguro e o que acontecerá a seguir. Um conjunto previsível de estados evita pânico e tickets de suporte.
Comece com um modelo de status pequeno e visível e mantenha-o consistente entre telas:
- Queued: salvo no dispositivo, aguardando rede
- Syncing: enviando agora
- Sent: confirmado pelo servidor
- Failed: não pôde ser enviado, vai re-tentar ou precisa de atenção
- Needs review: enviado, mas o servidor rejeitou ou sinalizou
Reenvios devem economizar bateria e dados. Use tentativas rápidas no início (para lidar com quedas breves), depois desacelere. Um backoff simples como 1 min, 5 min, 15 min e depois a cada hora é fácil de raciocinar. Também reenvie só quando fizer sentido (não fique re-enviando uma mudança inválida).
Trate erros de forma diferente, porque a próxima ação é distinta:
- Offline / sem rede: fica enfileirado, reintenta quando estiver online
- Timeout / servidor indisponível: marca como failed, auto-reenvia com backoff
- Autenticação expirada: pausa a sincronização e pede para o usuário entrar novamente
- Validação falhou (dados inválidos): precisa revisão, mostre o que corrigir
- Conflito (registro mudou): precisa revisão, encaminhe para suas regras de conflito
Idempotência é o que mantém reenvios seguros. Cada mudança deve ter um ID único de ação (frequentemente um UUID) que é enviado com a requisição. Se o app re-enviar a mesma mudança, o servidor deve reconhecer o ID e retornar o mesmo resultado em vez de criar duplicados.
Exemplo: um técnico salva um serviço concluído offline e entra no elevador. O app envia a atualização, dá timeout e reenvia depois. Com um action ID, o segundo envio é inofensivo. Sem ele, você pode criar eventos duplicados de “concluído”.
No AppMaster, trate esses estados e regras como campos e lógica de primeira classe no seu processo de sincronização, para que seus apps Kotlin e SwiftUI se comportem igual em todos os lugares.
UX de alterações pendentes: o que o usuário vê e pode fazer
As pessoas devem se sentir seguras usando o app offline. Uma boa UX de “alterações pendentes” é calma e previsível: reconhece que o trabalho está salvo no dispositivo e torna o próximo passo óbvio.
Um indicador sutil funciona melhor que um banner de alerta. Por exemplo, mostre um pequeno ícone “Sincronizando” no cabeçalho, ou um discreto rótulo “3 pendentes” na tela onde as edições ocorrem. Use cores chamativas só para perigo real (como “não foi possível enviar porque você saiu da conta”).
Dê ao usuário um lugar único para entender o que está acontecendo. Uma tela simples de Outbox ou Alterações pendentes pode listar itens com linguagem clara como “Comentário adicionado ao Ticket 104” ou “Foto de perfil atualizada.” Essa transparência evita pânico e reduz chamados ao suporte.
O que os usuários podem fazer
A maioria das pessoas só precisa de poucas ações, e elas devem ser consistentes pelo app:
- Reenviar agora
- Editar novamente (cria uma mudança mais recente)
- Descartar a mudança local
- Copiar detalhes (útil ao reportar um problema)
Mantenha rótulos de status simples: Pending, Syncing, Failed. Quando algo falha, explique como uma pessoa diria: “Não foi possível enviar. Sem internet.” ou “Rejeitado porque esse registro foi alterado por outra pessoa.” Evite códigos de erro.
Não bloqueie o app inteiro
Trave apenas ações que realmente exigem estar online, como “Pagar com Stripe” ou “Convidar um novo usuário.” Todo o resto deve continuar funcionando, incluindo ver dados recentes e criar novos rascunhos.
Um fluxo realista: um técnico de campo edita um relatório no porão. O app mostra “1 pendente” e deixa ele continuar trabalhando. Depois, muda para “Sincronizando” e limpa automaticamente. Se falhar, o relatório continua disponível, marcado como “Fail”, com um único botão “Reenviar agora”.
Se você está construindo no AppMaster, modele esses estados como parte de cada registro (pending, failed, synced) para que a UI reflita isso em todos os lugares sem telas de caso especial.
Autenticação, permissões e segurança no offline
O modo offline muda seu modelo de segurança. Um usuário pode tomar ações sem conexão, mas seu servidor continua sendo a fonte da verdade. Trate toda mudança enfileirada como “solicitada”, não “aprovada”.
Expiração de login enquanto offline
Tokens expiram. Quando isso acontece offline, deixe o usuário continuar criando edições e armazene-as como pendentes. Não finja que ações que exigem confirmação do servidor (como pagamentos ou aprovações administrativas) foram concluídas. Marque-as como pendentes até a próxima atualização de autenticação bem-sucedida.
Quando o app voltar online, tente um refresh silencioso primeiro. Se precisar pedir para o usuário entrar de novo, faça isso uma vez e então retome a sincronização automaticamente.
Após o re-login, revalide cada item enfileirado antes de enviá-lo. A identidade do usuário pode ter mudado (dispositivo compartilhado) e edições antigas não devem sincronizar sob a conta errada.
Mudanças de permissão e ações proibidas
Permissões podem mudar enquanto o usuário está offline. Uma edição permitida ontem pode ser proibida hoje. Trate isso explicitamente:
- Re-verifique permissões no servidor para cada ação enfileirada
- Se for proibido, pare esse item e mostre o motivo claramente
- Mantenha a edição local para que o usuário possa copiá-la ou pedir acesso
- Evite reenvios repetidos para erros “forbidden”
Exemplo: um agente de suporte edita uma nota de cliente offline num voo. Durante a noite, o papel dele é removido. Quando a sincronização roda, o servidor rejeita a atualização. O app deve mostrar “Não foi possível enviar: você não tem mais acesso” e manter a nota como rascunho local.
Dados sensíveis armazenados offline
Armazene o mínimo necessário para renderizar telas e reproduzir a fila. Encripte o armazenamento offline, evite cachear segredos e defina regras claras para logout (por exemplo: apagar dados locais, ou manter rascunhos somente com consentimento explícito do usuário). Se você está construindo com AppMaster, comece com o módulo de autenticação e projete sua fila para que ela sempre aguarde uma sessão válida antes de enviar mudanças.
Armadilhas comuns que causam perda de trabalho ou registros duplicados
A maioria dos bugs offline não é sofisticada. Vem de algumas decisões pequenas que parecem inofensivas em Wi‑Fi perfeito e quebram no trabalho real.
Uma falha comum é sobrescritas silenciosas. Se o app fizer upload de uma versão mais antiga e o servidor aceitá-la sem checar, você pode apagar a edição mais nova de outra pessoa e ninguém percebe até tarde demais. Sincronize com um número de versão (ou timestamp updatedAt) e recuse sobrescrever quando o servidor avançou, para que o usuário tenha uma escolha clara.
Outra armadilha é a tempestade de reenvios. Quando um telefone reconecta com sinal fraco, o app pode bombardear o backend a cada poucos segundos, drenando bateria e criando escritas duplicadas. Reenvios devem ser calmos: desacelere após cada falha e adicione um pouco de aleatoriedade para que milhares de dispositivos não reintem ao mesmo tempo.
Os erros que mais levam a trabalho perdido ou duplicados:
- Tratar toda falha como “rede”: separe erros permanentes (dados inválidos, permissão faltante) de temporários (timeout).
- Esconder falhas de sincronização: se as pessoas não veem o que falhou, elas refeitas a tarefa e criam dois registros.
- Enviar a mesma mudança duas vezes sem proteção: sempre anexe um request ID único para que o servidor reconheça e ignore duplicados.
- Mesclar campos de texto automaticamente sem avisar: se você combinar edições automaticamente, deixe os usuários revisar o resultado quando importar.
- Criar registros offline sem um ID estável: use um ID local temporário e mapeie para o ID do servidor após o upload, assim edições posteriores não criam uma segunda cópia.
Um exemplo rápido: um técnico de campo cria um novo “Site Visit” offline e depois o edita duas vezes antes de reconectar. Se a chamada de criação for re-tentada e gerar dois registros no servidor, as edições posteriores podem se ligar ao registro errado. IDs estáveis e deduplicação no servidor evitam isso.
Se você está construindo com AppMaster, as regras não mudam. A diferença é onde implementá-las: na lógica de sincronização, no modelo de dados e nas telas que mostram “failed” vs “sent”.
Cenário de exemplo: duas pessoas editam o mesmo registro
Uma técnica de campo, Maya, está atualizando o ticket “Job #1842” num porão sem sinal. Ela altera o status de “Em andamento” para “Concluído” e adiciona uma nota: “Substituiu a válvula, testado ok.” O app salva instantaneamente e mostra como pendente.
Lá em cima, seu colega Leo está online e edita o mesmo job ao mesmo tempo. Ele muda o horário agendado e atribui o job a outro técnico, porque um cliente ligou com uma atualização.
Quando Maya recupera sinal, a sincronização em background começa silenciosamente. Eis o que acontece num fluxo previsível e amigável ao usuário:
- A mudança de Maya ainda está na fila (ID do job, campos alterados, timestamp e a versão que ela viu).
- O app tenta enviar. O servidor responde: “Este job foi atualizado desde a versão que você tinha” (conflito).
- Sua regra de conflito roda: status e notas podem ser mesclados, mas mudanças de atribuição vencem se foram feitas mais tarde no servidor.
- O servidor aceita um resultado mesclado: status = “Concluído” (de Maya), nota adicionada (de Maya), técnico atribuído = escolha do Leo (de Leo).
- O job reabre no app da Maya com um banner claro: “Sincronizado com atualizações. A atribuição mudou enquanto você estava offline.” Uma ação pequena “Ver” mostra o que mudou.
Agora adicione um momento de falha: o token de login da Maya expirou enquanto ela estava offline. A primeira tentativa de sincronização falha com “É necessário entrar.” O app mantém as edições, marca como “Pausado” e mostra um único prompt. Depois que ela entra novamente, a sincronização retoma automaticamente sem que ela precise reescrever nada.
Se houver um problema de validação (por exemplo, “Concluído” requer uma foto), o app não deve adivinhar. Marca o item como “Precisa de atenção”, diz exatamente o que adicionar e deixa ela reenviar.
Plataformas como AppMaster ajudam aqui porque você pode desenhar a fila, regras de conflito e o estado pendente visualmente, ainda entregando apps nativos reais em Kotlin e SwiftUI.
Checklist rápido e próximos passos
Trate sincronização offline como um recurso ponta a ponta que você pode testar, não como uma pilha de correções. O objetivo é simples: os usuários nunca se perguntem se o trabalho está salvo e o app não crie duplicados surpresa.
Uma checklist curta para confirmar a base:
- A fila de sincronização é armazenada no dispositivo e cada mudança tem um ID local estável mais um ID do servidor quando disponível.
- Existem estados claros (queued, syncing, sent, failed, needs review) e são usados de forma consistente.
- Requisições são idempotentes (seguras para reenvio) e cada operação inclui uma chave de idempotência.
- Registros têm versionamento (updatedAt, número de revisão ou ETag) para que conflitos sejam detectados.
- Regras de conflito estão escritas em linguagem simples (quem vence, o que mescla, quando perguntar ao usuário).
Depois disso, verifique se a experiência é tão forte quanto o modelo de dados. Usuários devem poder ver o que está pendente, entender o que falhou e agir sem medo de perder trabalho.
Teste com cenários que imitam a vida real:
- Modo avião: criar, atualizar, excluir, depois reconectar.
- Rede instável: cair a conexão no meio da sincronização e garantir que reenvios não dupliquem.
- App morto: forçar fechamento durante o envio, reabrir e confirmar que a fila recupera.
- Hora incorreta: dispositivo com relógio errado, confirmar que detecção de conflito ainda funciona.
- Toques duplicados: usuário toca Salvar duas vezes, confirmar que vira uma só mudança no servidor.
Prototipe o fluxo completo antes de polir a UI. Construa uma tela, um tipo de registro e um caso de conflito (duas edições no mesmo campo). Adicione uma área de status de sincronização simples, um botão Reenviar para falhas e uma tela clara de conflito. Quando isso funcionar, repita para mais telas.
Se você está construindo sem programar, AppMaster (appmaster.io) pode gerar apps nativos Kotlin e SwiftUI junto com o backend, para que você foque na fila, checagens de versão e estados visíveis ao usuário em vez de ligar tudo à mão.


