Como lidar com offline-first em apps mobile com sincronização
1. Fundamentos do Offline-First em Dispositivos Móveis
O design offline-first prioriza a funcionalidade completa do aplicativo mesmo sem conexão com a internet, tratando a rede como uma melhoria, não como um requisito. Diferente de abordagens offline-only (que nunca sincronizam) ou online-only (que exigem conexão constante), o offline-first oferece uma experiência híbrida: o usuário interage com dados locais e, quando a rede está disponível, ocorre a sincronização automática com o servidor.
Casos de uso críticos incluem aplicativos de mensagens (WhatsApp, Telegram), ferramentas de produtividade (Notion, Todoist) e plataformas de e-commerce (Shopify, Amazon) — onde a perda de funcionalidade durante quedas de rede resulta em frustração e abandono do usuário.
Princípios do Offline-First:
1. O app deve funcionar completamente offline
2. Dados locais são a fonte primária de verdade
3. Sincronização ocorre de forma assíncrona e transparente
4. Conflitos são resolvidos de maneira previsível
5. Feedback visual informa o estado da sincronização
2. Arquitetura de Dados para Sincronização
A modelagem de dados precisa suportar detecção e resolução de conflitos. Timestamps Unix (millissegundos desde 1970) são a abordagem mais simples para last-write-wins. Para cenários mais complexos, vetores de versão (version vectors) permitem rastrear alterações concorrentes em múltiplos dispositivos.
Quanto ao armazenamento local, SQLite (via SQLiteNet ou Room) oferece suporte robusto a transações e consultas complexas. Realm é otimizado para performance mobile, mas possui limitações de licenciamento. AsyncStorage (React Native) e MMKV (React Native/Flutter) são opções leves para dados simples, mas não substituem bancos relacionais para sincronização avançada.
Exemplo: Estrutura de fila de operações pendentes
{
"pendingOperations": [
{
"id": "op-001",
"type": "UPDATE",
"entity": "product",
"entityId": "prod-123",
"data": { "price": 29.90 },
"timestamp": 1712345678000,
"retryCount": 0,
"status": "pending"
}
]
}
3. Gerenciamento de Estado e Cache Local
Bibliotecas como Redux Offline, TanStack Query e WatermelonDB abstraem grande parte da complexidade do offline-first. Redux Offline gerencia filas de ações e sincronização automática. TanStack Query oferece cache inteligente com estratégias como stale-while-revalidate (usa dados em cache enquanto busca atualizações em segundo plano). WatermelonDB é especializado em sincronização com SQLite e suporte nativo a conflitos.
Estratégias de cache incluem:
- Cache-first: retorna dados locais imediatamente, sincroniza em segundo plano
- Stale-while-revalidate: exibe dados desatualizados enquanto atualiza em background
- Network-first: tenta buscar do servidor, cai para cache offline se falhar
Configuração TanStack Query com offline-first:
{
staleTime: 5 * 60 * 1000, // 5 minutos
cacheTime: 30 * 60 * 1000, // 30 minutos
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
networkMode: 'offlineFirst'
}
4. Estratégias de Sincronização de Dados
A sincronização pull pode ser implementada via requisições periódicas (polling a cada 30 segundos) ou notificações push (mais eficiente, mas requer infraestrutura de servidor). A sincronização push envia dados locais para o servidor — idealmente em lotes para reduzir overhead de rede, com retry exponencial em caso de falha.
Delta sync é essencial para performance: em vez de transferir todo o banco de dados, apenas as alterações desde o último sync são enviadas. Isso reduz drasticamente o tráfego e o tempo de sincronização.
Algoritmo de Delta Sync:
1. Cliente envia timestamp do último sync bem-sucedido
2. Servidor retorna apenas registros modificados após esse timestamp
3. Cliente aplica alterações localmente
4. Cliente envia operações pendentes (fila local)
5. Servidor processa e retorna confirmação com novos timestamps
5. Resolução de Conflitos e Consistência
O modelo last-write-wins (LWw) é o mais simples: o registro com timestamp mais recente vence. Para dados colaborativos (como documentos editados por múltiplos usuários), CRDTs (Conflict-free Replicated Data Types) permitem que cada dispositivo opere de forma independente e os resultados sejam mesclados automaticamente sem conflitos.
Para cenários onde a resolução automática não é possível, implemente resolução manual: exiba ambas as versões ao usuário e permita escolher ou fazer merge.
Exemplo: Resolução de conflito com merge automático
function resolveConflict(local, remote) {
if (local.timestamp > remote.timestamp) return local;
if (remote.timestamp > local.timestamp) return remote;
// Timestamps iguais: merge de campos não conflitantes
return { ...local, ...remote };
}
6. Gerenciamento de Conectividade e Transições
Monitore o estado da rede com bibliotecas como NetInfo (React Native) ou Connectivity (Flutter). Ao detectar perda de conexão, ative o modo offline e armazene operações na fila pendente. Ao reconectar, execute a sincronização em lote, garantindo reautenticação segura (tokens de acesso válidos).
Forneça feedback visual claro: indicador de status (conectado/sincronizando/offline), notificações de sincronização concluída e alertas de conflitos pendentes.
Monitoramento de conectividade com NetInfo:
NetInfo.addEventListener(state => {
if (state.isConnected) {
processPendingOperations();
startSyncPull();
updateUIStatus('online');
} else {
updateUIStatus('offline');
}
});
7. Performance e Otimização
Comprima dados durante a sincronização (GZIP para JSON, protobuf para dados binários). Implemente paginação: sincronize em blocos de 100-500 registros para evitar sobrecarga de memória. Limite requisições concorrentes a 3-5 simultâneas para evitar congestionamento de rede.
Use debounce para operações offline frequentes (como digitação em formulários): aguarde 500ms de inatividade antes de enfileirar a operação. Para sincronização em segundo plano, utilize workers (Web Workers em React Native, isolates em Flutter) ou bibliotecas como react-native-background-fetch.
Debounce para operações offline:
let debounceTimer;
function handleInputChange(value) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
addToPendingQueue('UPDATE_FORM', { value });
}, 500);
}
8. Testes e Monitoramento em Cenários Offline
Simule condições de rede usando modo avião, ferramentas de throttling (Charles Proxy, Flipper) ou bibliotecas como Mock Service Worker (MSW). Testes de integração devem validar: fila de operações persiste após reinicialização do app, conflitos são resolvidos corretamente e dados locais são consistentes após sincronização.
Monitore métricas de sincronização: taxa de sucesso (percentual de operações enviadas com sucesso), latência média de sincronização e número de conflitos resolvidos automaticamente vs. manualmente.
Métricas de sincronização para monitoramento:
{
"syncSuccessRate": 0.97,
"avgSyncLatency": 1200, // ms
"pendingOperations": 5,
"conflictsAutoResolved": 42,
"conflictsManualResolved": 3,
"lastSyncTimestamp": 1712345678000
}
Referências
- Documentação oficial do WatermelonDB — Guia completo sobre sincronização offline-first com SQLite e suporte a conflitos
- TanStack Query: Offline Mutations — Tutorial sobre filas de operações offline e retry automático
- Redux Offline: Arquitetura e Exemplos — Repositório oficial com documentação e exemplos de sincronização offline
- CRDTs: Conflict-free Replicated Data Types explicados — Introdução teórica e prática a CRDTs para sincronização colaborativa
- NetInfo: Monitoramento de conectividade em React Native — Biblioteca oficial para detecção de estado de rede
- Google: Offline-First Design Patterns — Padrões de arquitetura offline-first para Android (aplicável a mobile em geral)
- SQLite vs Realm vs MMKV: Comparativo de Performance — Artigo técnico comparando soluções de armazenamento local para mobile