Una referencia compuesta para operaciones de campo offline-first - inspecciones de servicios, auditorías de almacén o encuestas de activos municipales. Los técnicos capturan fotos, coordenadas GPS y respuestas de listas de verificación sin conectividad confiable. Stack: Expo SDK 57 (expo ~57.0.4), React Native 0.86.0, React 19.2.3.
El 40% del trabajo ocurre en sótanos, subestaciones y sitios rurales con sin señal durante horas.
Perder una inspección completada porque la aplicación falló antes de sincronizarse es un fallo de contrato, no una molestia UX.
Los dispositivos son Android robusto (Zebra TC57, Honeywell CT40) más una minoría de iPhones corporativos.
El backend acepta cargas por lotes REST; la política de conflictos es autorizada por servidor en timestamps de aprobación.
Restricciones del equipo: Cuatro ingenieros móviles, un ingeniero backend, sin especialista nativo on-call. Flujo de trabajo administrado por Expo con distribución interna EAS a dispositivos de flota.
┌──────────────────────────────────────────────────────────────────────────┐│ Aplicación de campo - plano de datos offline-first │├──────────────────────────────────────────────────────────────────────────┤│ Expo Router ││ (app)/jobs/[jobId]/inspection/[id] ← enlace profundo desde SMS │├──────────────────────────────────────────────────────────────────────────┤│ Capa UI ││ DataFreshnessLabel (live|stale|syncing|unavailable) ││ Guardado optimista → SQLite inmediatamente │├──────────────────────────────────────────────────────────────────────────┤│ expo-sqlite (fuente de verdad) ││ inspections, answers, attachments_meta ││ outbox (cola de mutaciones, idempotency_key, payload_json) ││ sync_cursor (last_server_seq) │├──────────────────────────────────────────────────────────────────────────┤│ TanStack Query ││ lista de trabajos / datos de referencia - stale-while-revalidate │├──────────────────────────────────────────────────────────────────────────┤│ NetInfo → onlineManager + vaciado en primer plano ││ expo-background-task → drenaje de outbox diferido (intervalo ≥15 min) │├──────────────────────────────────────────────────────────────────────────┤│ expo-file-system + expo-image ││ archivos adjuntos en disco; cargar en worker en segundo plano │└──────────────────────────────────────────────────────────────────────────┘
// src/sync/outboxFlush.ts (extracto)export async function flushOutbox(db: SQLiteDatabase) { const rows = await db.getAllAsync<OutboxRow>( `SELECT * FROM outbox ORDER BY created_at LIMIT 50` ); for (const row of rows) { const res = await api.post("/inspections", row.payload_json, { headers: { "Idempotency-Key": row.idempotency_key }, }); if (res.ok) await markSynced(db, row.id); }}
// index.ts - registrar tarea en segundo plano en scope globalimport "./src/background/syncTask";import "expo-router/entry";
Laboratorio de dispositivos mínimo: Un Android robusto en Android 11+, un iPhone de dos generaciones anteriores al actual, ambos en compilaciones release - no Expo Go.
Recuperación de corrupción: un dispositivo con OTA fallida a mitad de la migración activó cuarentena safeParse - la aplicación renderizó inspecciones última-conocida-buena sin pantalla blanca por Data Layer Best Practices #13.
Migraciones SQLite antes de primer renderizado. Un bump de user_version enviado en una OTA sin compilación de almacén rompió arranque en frío una vez - ahora los cambios de schema nativo requieren bump binario.
Mostrar estado pendiente por fila, no spinner global. Los inspectores trabajan en trabajo B mientras trabajo A se carga; el indicador synced por fila superó un modal de bloqueo.
Los dispositivos robustos mienten sobre batería.BatterySaver en Zebra retrasó tareas en segundo plano; documenta que el vaciado en primer plano es la ruta de entrega real.
Las cargas de fotos necesitan progreso de bytes. Carga de expo-file-system con onUploadProgress redujo llamadas "¿se guardó?" a despacho.
Cierre de sesión limpia todas las capas. Caché de inspección obsoleta en dispositivos compartidos fue una clase de incidente de privacidad - queryClient.clear() + tablas de usuario SQLite + directorios de adjuntos.
Fijar SDK 57 en runbooks. TI de campo instala APKs meses después; documentos de soporte deben indicar qué runtimeVersion acepta qué OTA.