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