expo-background-task e expo-task-manager para uploads adiados. Os sistemas operacionais móveis encerram o trabalho de rede em primeiro plano quando os usuários mudam de aplicativo. Registre um worker em segundo plano para esvaziar caixas de saída SQLite, fazer upload de anexos e buscar deltas - dentro de limites rigorosos de bateria e agendamento.
// src/background/syncTask.ts - importe este arquivo na entrada do aplicativo (escopo global)import * as BackgroundTask from "expo-background-task";import * as TaskManager from "expo-task-manager";export const SYNC_TASK = "offline-sync-worker";TaskManager.defineTask(SYNC_TASK, async () => { try { // Abra o banco de dados, esvazie a caixa de saída, faça upload de arquivos - mantenha o trabalho curto console.log("[sync] background worker ran"); return BackgroundTask.BackgroundTaskResult.Success; } catch (error) { console.error("[sync] background worker failed", error); return BackgroundTask.BackgroundTaskResult.Failed; }});
// src/background/registerSyncTask.tsimport * as BackgroundTask from "expo-background-task";import * as TaskManager from "expo-task-manager";import { SYNC_TASK } from "./syncTask";export async function registerSyncTask() { const status = await BackgroundTask.getStatusAsync(); if (status === BackgroundTask.BackgroundTaskStatus.Restricted) return; const registered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK); if (!registered) { await BackgroundTask.registerTaskAsync(SYNC_TASK, { minimumInterval: 15, // minutos - o SO pode atrasar ainda mais }); }}
// index.ts ou app/_layout.tsx - importação de efeito colateral no topoimport "@/background/syncTask";
Quando usar isso:
Esvaziar a caixa de saída pendente do SQLite / AsyncStorage quando o aplicativo estiver em segundo plano
Fazer upload de fotos de inspeção após a captura em campo
Puxar catálogo incremental que não deve bloquear a UX em primeiro plano
Quando evitar:
O usuário está esperando confirmação imediata - esvazie na reconexão em primeiro plano
Sondagem de sub-minuto - o SO não a honrará; use notificações push ou refetch em primeiro plano
// src/background/syncTask.tsimport * as BackgroundTask from "expo-background-task";import * as TaskManager from "expo-task-manager";import * as SQLite from "expo-sqlite";import { flushWorkOrders } from "@/sync/flushOutbox";export const SYNC_TASK = "offline-sync-worker";TaskManager.defineTask(SYNC_TASK, async () => { try { const db = await SQLite.openDatabaseAsync("field.db"); // flushWorkOrders sem QueryClient - o background não tem árvore React await flushWorkOrders(db, null); return BackgroundTask.BackgroundTaskResult.Success; } catch (error) { console.error("[sync] failed", error); return BackgroundTask.BackgroundTaskResult.Failed; }});
// src/sync/flushOutbox.ts (excerto) - tolera queryClient nulo em segundo planoexport async function flushWorkOrders( db: SQLiteDatabase, queryClient: QueryClient | null) { // ... enviar linhas pendentes ... if (queryClient) { await queryClient.invalidateQueries({ queryKey: ["work-orders"] }); }}
// app/_layout.tsximport "@/background/syncTask"; // efeito colateral global defineTaskimport { Stack } from "expo-router";import { useEffect } from "react";import * as BackgroundTask from "expo-background-task";import { registerSyncTask } from "@/background/registerSyncTask";import { setupMobileQuery } from "@/lib/setupMobileQuery";export default function RootLayout() { useEffect(() => { setupMobileQuery(); registerSyncTask(); }, []); return <Stack />;}
// src/dev/TriggerBackgroundSync.tsx - apenas para desenvolvimentoimport * as BackgroundTask from "expo-background-task";import { Button } from "react-native";export function TriggerBackgroundSync() { if (!__DEV__) return null; return ( <Button title="Test background sync" onPress={() => BackgroundTask.triggerTaskWorkerForTestingAsync()} /> );}
// app.config.ts (excerto) - CNG aplica automaticamente as chaves BGTaskScheduler do iOSexport default { expo: { name: "FieldApp", plugins: [ "expo-router", // O plugin de configuração expo-background-task aplica UIBackgroundModes + BGTaskSchedulerPermittedIdentifiers ], },};
O Expo prebuild / CNG aplica isso através do plugin expo-background-task. Projetos bare precisam editar Info.plist manualmente. Simuladores não executam BGTaskScheduler - teste em um dispositivo.
minimumInterval: 15 é o limite inferior em minutos. O WorkManager também espera restrições de rede + bateria - sua tarefa é executada quando as condições se alinham, não exatamente em T+15.
Pilha de sincronização recomendada:1. UI otimista + caixa de saída na ação do usuário (primeiro plano)2. NetInfo reconectar → flush imediato (primeiro plano)3. BackgroundTask → esvaziar o restante quando o aplicativo estiver em segundo plano4. "Sincronizar agora" manual para supervisores de campo
O segundo plano é uma rede de segurança - não o caminho de entrega principal.
defineTask dentro de um componente - Tarefa indefinida na inicialização fria do SO. Correção: Importação de módulo global na entrada.
Esperando execução imediata - A primeira execução pode levar horas no iOS. Correção: Flush em primeiro plano na reconexão; segundo plano para o restante.
Testando apenas no Simulador iOS - Tarefas em segundo plano nunca são acionadas. Correção: Dispositivo físico + triggerTaskWorkerForTestingAsync em __DEV__.
Trabalho síncrono longo em defineTask - O iOS encerra tarefas que excedem o orçamento. Correção: Lote de uploads; escute a expiração; retorne Success cedo.
Usando hooks React dentro de defineTask - Nenhuma árvore React na entrada de segundo plano. Correção: Abra SQLite / AsyncStorage diretamente; sem useQueryClient.
Esquecendo prebuild após adicionar plugin - Console: No task request with identifier … scheduled. Correção:npx expo prebuild ou EAS build com configuração nativa atualizada.
Múltiplas tarefas registradas - O Expo usa um único worker nativo - o último registro vence o intervalo mínimo. Correção: Um defineTask orquestrando todo o trabalho adiável.
Requer um build de desenvolvimento para comportamento nativo completo - verifique no dispositivo após npx expo prebuild.
Por que defineTask deve ser global?
Quando iOS/Android acorda seu aplicativo para um evento em segundo plano, o React pode não estar montado. O runtime nativo procura a tarefa pelo nome no registro global do TaskManager - o registro dentro de um componente é executado tarde demais ou não é executado.
Qual minimumInterval devo usar?
Comece com 15 (minutos) - o mínimo da plataforma. O iOS ainda pode adiar para janelas noturnas. Não use tarefas em segundo plano para requisitos de atualização com menos de 15 minutos.
Apenas em builds de desenvolvimento. Mova o aplicativo para segundo plano no Android antes de forçar a execução do trabalho via adb (veja a documentação do Expo).
A sincronização em segundo plano é executada quando o aplicativo é forçado a fechar?
iOS: Deslizar para fechar no seletor de aplicativos encerra o aplicativo - tarefas agendadas são retomadas após o próximo lançamento pelo usuário registrá-las novamente.
Projete para retomar na próxima abertura + durabilidade da caixa de saída no SQLite.
Posso usar TanStack Query na tarefa em segundo plano?
Não diretamente - nenhum provedor na entrada de segundo plano. Esvazie os dados em código imperativo; invalide o cache do Query na próxima vez que o usuário abrir o aplicativo em primeiro plano.
Qual valor de retorno defineTask deve usar?
Retorne BackgroundTask.BackgroundTaskResult.Success quando o trabalho for concluído ou nada estiver pendente. Retorne Failed quando os dados ainda estiverem pendentes e você quiser que o SO considere a retentativa - combine com lógica de flush idempotente.
Como isso se relaciona com as tarefas de localização do expo-task-manager?
Mesma API TaskManager.defineTask - fontes de gatilho diferentes. A sincronização em segundo plano usa o agendamento do expo-background-task; a localização usa APIs do expo-location. Mantenha os nomes das tarefas exclusivos por tipo de worker.