La mayoría de tutoriales de "offline en Flutter" enseñan a cachear una respuesta de red y mostrarla cuando no hay conexión. Eso no es offline-first: es una degradación. Offline-first es una decisión de arquitectura en la que la app funciona sin internet por diseño, y la conectividad es un detalle de infraestructura invisible para quien la usa. Este artículo explica cómo lo construimos en producción.

¿Qué significa realmente offline-first?

En una app online convencional, la fuente de verdad está en el servidor y el dispositivo es una ventana que se queda en blanco cuando falla la red. En una app offline-first, la fuente de verdad es la base de datos local del dispositivo: la interfaz lee y escribe siempre contra esos datos locales —por eso responde en milisegundos— y un motor de sincronización se encarga de reconciliar los cambios con el backend cuando hay conexión disponible. El usuario crea un albarán, firma una entrega o registra una incidencia sin cobertura, y todo eso sube intacto cuando vuelve la red.

La parte fácil: la base de datos local

En Flutter usamos SQLite gestionado con Drift como base de datos local, con migraciones versionadas entre releases y SQLCipher para cifrado en reposo cuando hay datos sensibles. La UI nunca espera a la red: escribe en local, la escritura es instantánea, y el usuario sigue trabajando. Hasta aquí, poco misterio. El problema aparece después.

La parte difícil: reconciliar sin perder datos

Si dos dispositivos han editado el mismo registro mientras estaban desconectados, ¿qué versión gana cuando ambos vuelven a tener red? Responder "la última" es tentador y casi siempre incorrecto: el reloj de un móvil puede ir atrasado, y "última" según qué reloj no es una respuesta determinista. La reconciliación es donde las implementaciones offline mal hechas corrompen registros, duplican entradas o pierden escrituras del usuario.

Nuestra estrategia depende del dominio del dato:

  • CRDT cuando la fusión es automática. Los Conflict-free Replicated Data Types son estructuras diseñadas para converger al mismo estado sin coordinación central: contadores incrementales, sets de elementos, listas ordenadas, texto colaborativo. Dos réplicas que aplican las mismas operaciones en distinto orden acaban idénticas, por construcción matemática.
  • Last-write-wins con reloj lógico cuando el último cambio debe prevalecer. En lugar del reloj físico del dispositivo, usamos un reloj lógico (por ejemplo un contador de versión monótono o un timestamp híbrido) que ordena los eventos de forma determinista aunque los relojes de los dispositivos no estén sincronizados.
  • Merge por campos o resolución manual asistida para datos críticos de negocio, donde perder un cambio tiene coste real. Aquí no automatizamos la decisión: la mostramos, con contexto, y dejamos un registro de auditoría de cada reconciliación.

La regla que no rompemos nunca: una escritura del usuario no se descarta en silencio. Si hay un conflicto que no se puede resolver de forma segura, se conserva y se hace visible.

El motor de sincronización: colas, idempotencia y reintentos

Entre la base local y el backend vive el motor de sincronización. Su corazón es una cola de operaciones persistente: cada cambio local se encola como una operación (crear, actualizar, borrar) y se reintenta hasta confirmarse, sobreviviendo a cierres de la app y a cortes de red con backoff exponencial. Tres decisiones lo hacen fiable:

  • Identificadores generados en el cliente (UUID): el usuario puede crear un registro sin esperar un round-trip al servidor, porque el ID ya existe en local.
  • API idempotente: reenviar la misma operación —algo que ocurre constantemente tras un corte— nunca duplica el resultado. En el backend en Go sobre Cloud Run esto se implementa con claves de idempotencia y upserts.
  • Sincronización incremental: por timestamps o vectores de versión, no descargando todo el dataset en cada arranque. Solo viaja lo que cambió.

¿Offline-first y tiempo real son incompatibles?

No. La app trabaja siempre contra su copia local y, cuando hay conexión, propaga y recibe cambios en tiempo real por websockets o push. El usuario ve las actualizaciones de sus compañeros cuando hay red y sigue trabajando con sus datos locales cuando no la hay. La reconciliación con CRDT o reloj lógico garantiza que todos los dispositivos convergen al mismo estado final.

¿Cuándo necesitas realmente offline-first?

Siempre que tus usuarios no puedan depender de la cobertura: logística y transporte (repartidores, almacenes), field service y mantenimiento industrial, sanidad en zonas sin señal, agricultura remota, TPV en tienda, y cualquier app conectada a hardware IoT que genera datos sin internet. En esos escenarios, obligar al usuario a esperar al servidor —o bloquearle el trabajo sin red— es un fallo de producto.

Conclusión

Offline-first no es un plugin que se añade al final: reconvertir una app online a offline-first después cuesta mucho más que diseñarla así desde el primer día. La dificultad no está en guardar datos localmente, sino en reconciliarlos sin perder ni una escritura. Con base de datos local como fuente de verdad, una cola de operaciones idempotente y una estrategia de conflictos elegida por dominio, una app funciona igual de bien con red que sin ella. Si quieres verlo aplicado a tu caso, lo explicamos en nuestra página de desarrollo de apps offline-first.