expo-background-task y expo-task-manager para cargas diferidas. Los sistemas operativos móviles detienen el trabajo de red en primer plano cuando los usuarios cambian de aplicación. Registra un worker en segundo plano para vaciar las bandejas de salida de SQLite, subir archivos adjuntos y extraer deltas - dentro de límites estrictos de batería y programación.
// src/background/syncTask.ts - importa este archivo en la entrada de la aplicación (scope 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 { // Abre BD, vacía bandeja de salida, sube archivos - mantén el trabajo corto 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 - el SO puede retrasar más }); }}
// index.ts o app/_layout.tsx - importación de efecto secundario en la parte superiorimport "@/background/syncTask";
Cuándo usarlo:
Vaciar bandeja de salida pendiente de SQLite / AsyncStorage cuando la aplicación se pone en segundo plano
Subir fotos de inspección después de captura en campo
Extracción incremental de catálogo que no debería bloquear la UX en primer plano
Cuándo evitarlo:
El usuario espera confirmación inmediata - vacía al reconectarse en primer plano
Polling de menos de un minuto - el SO no lo honrará; usa notificaciones push o recargas en primer 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 sin QueryClient - el segundo plano no tiene árbol React await flushWorkOrders(db, null); return BackgroundTask.BackgroundTaskResult.Success; } catch (error) { console.error("[sync] failed", error); return BackgroundTask.BackgroundTaskResult.Failed; }});
// src/sync/flushOutbox.ts (extracto) - tolera queryClient nulo en segundo planoexport async function flushWorkOrders( db: SQLiteDatabase, queryClient: QueryClient | null) { // ... envía filas pendientes ... if (queryClient) { await queryClient.invalidateQueries({ queryKey: ["work-orders"] }); }}
// app/_layout.tsximport "@/background/syncTask"; // efecto secundario 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 - solo desarrolloimport * as BackgroundTask from "expo-background-task";import { Button } from "react-native";export function TriggerBackgroundSync() { if (!__DEV__) return null; return ( <Button title="Prueba sincronización en segundo plano" onPress={() => BackgroundTask.triggerTaskWorkerForTestingAsync()} /> );}
// app.config.ts (extracto) - CNG aplica automáticamente las claves de iOS BGTaskSchedulerexport default { expo: { name: "FieldApp", plugins: [ "expo-router", // el plugin de configuración de expo-background-task aplica UIBackgroundModes + BGTaskSchedulerPermittedIdentifiers ], },};
Expo prebuild / CNG aplica estos a través del plugin expo-background-task. Los proyectos bare deben editar Info.plist manualmente. Los simuladores no ejecutan BGTaskScheduler - prueba en dispositivo.
minimumInterval: 15 es el mínimo en minutos. WorkManager también espera restricciones de red + batería - tu tarea se ejecuta cuando se alinean las condiciones, no exactamente en T+15.
Pila de sincronización recomendada:1. UI optimista + bandeja de salida en acción del usuario (primer plano)2. NetInfo reconecta - vacío inmediato (primer plano)3. BackgroundTask - vacía el resto cuando la aplicación se pone en segundo plano4. "Sincronizar ahora" manual para supervisores de campo
El segundo plano es una red de seguridad - no la ruta de entrega principal.
defineTask dentro de un componente - Tarea indefinida en arranque en frío desde el SO. Solución: Importación de módulo global en entrada.
Esperando ejecución inmediata - La primera ejecución puede ser horas después en iOS. Solución: Vacía en primer plano al reconectar; segundo plano para residuos.
Pruebas solo en iOS Simulator - Las tareas de BG nunca se activan. Solución: Dispositivo físico + triggerTaskWorkerForTestingAsync en __DEV__.
Trabajo síncrono largo en defineTask - iOS detiene tareas que superan el presupuesto. Solución: Cargas por lotes; escucha la expiración; retorna Success temprano.
Usar hooks de React dentro de defineTask - Sin árbol React en la entrada de segundo plano. Solución: Abre SQLite / AsyncStorage directamente; sin useQueryClient.
Olvidar prebuild después de agregar el plugin - Consola: No task request with identifier … scheduled. Solución:npx expo prebuild o compilación de EAS con configuración nativa actualizada.
Múltiples tareas registradas - Expo usa un solo worker nativo - la última inscripción gana intervalo mínimo. Solución: Un defineTask orquestando todo el trabajo diferible.
Requiere una compilación de desarrollo para un comportamiento nativo completo - verifica en dispositivo después de npx expo prebuild.
¿Por qué defineTask debe ser global?
Cuando iOS/Android despierta tu aplicación para un evento de segundo plano, React puede no estar montado. El runtime nativo busca la tarea por nombre en el registro global de TaskManager - el registro dentro de un componente se ejecuta demasiado tarde o no se ejecuta en absoluto.
¿Qué minimumInterval debo usar?
Comienza con 15 (minutos) - el mínimo de plataforma. iOS aún puede diferir a ventanas nocturnas. No uses tareas en segundo plano para requisitos de actualización de menos de 15 minutos.
Solo compilaciones de desarrollo. Mueve la aplicación al segundo plano en Android antes de forzar la ejecución del trabajo mediante adb (ver docs de Expo).
¿Se ejecuta la sincronización en segundo plano cuando se cierra forzosamente la aplicación?
iOS: Deslizar en el conmutador de aplicaciones termina la aplicación - las tareas programadas se reanudan después del siguiente lanzamiento del usuario que las registra de nuevo.
Android: El comportamiento varía según el OEM - ver dontkillmyapp.com.
Diseña para reanudar en próxima apertura + durabilidad de bandeja de salida en SQLite.
¿Puedo usar TanStack Query en la tarea en segundo plano?
No directamente - sin proveedor en la entrada de segundo plano. Vacía datos en código imperativo; invalida el cache de Query la próxima vez que el usuario abra la aplicación en primer plano.
¿Qué valor de retorno debe usar defineTask?
Retorna BackgroundTask.BackgroundTaskResult.Success cuando el trabajo se completó o no había nada pendiente. Retorna Failed cuando los datos aún están pendientes y quieres que el SO considere un reintento - empareja con lógica de vacío idempotente.
¿Cómo se relaciona esto con tareas de ubicación de expo-task-manager?
Mismo API TaskManager.defineTask - diferentes fuentes de desencadenante. La sincronización en segundo plano usa programación expo-background-task; la ubicación usa APIs expo-location. Mantén nombres de tareas únicos por tipo de worker.