Saltar al contenido
móvil / · 6 min

Offline-first en React Native: la pregunta no es cómo, es para quién

235,613 registros no caben en un teléfono. Pero el promotor que los captura sólo necesita los suyos. Cómo el volumen por rol define la arquitectura, y por qué la captura sí es offline para todos.

Administrador · Desarrollador Laravel
Offline-first en React Native: la pregunta no es cómo, es para quién

La conversación sobre offline-first casi siempre empieza mal. «¿La app va a funcionar sin internet?» es una pregunta binaria para un problema que no lo es.

En la app de campo de una plataforma territorial la respuesta correcta resultó ser: depende de quién la abra.

El número que decide

La plataforma tiene 235,613 personas registradas.

Bajar eso a un teléfono no es imposible —SQLite lo aguanta— pero es una descarga inicial de varios minutos, un consumo de datos que nadie va a pagar, y una sincronización que hay que resolver para un conjunto que el usuario jamás va a mirar entero.

Y no hace falta. Un promotor de campo tiene decenas o cientos de personas a su alcance. Un coordinador, cientos o miles. Sólo el administrador ve las 235,613, y él trabaja sentado frente a una computadora con wifi.

De ahí sale la tabla que gobierna toda la arquitectura:

Rol Registros a su alcance Modo
Promotor decenas a cientos Offline completo. Se baja todo lo suyo
Coordinador cientos a miles Offline completo con tope
Administrador 235,613 En línea con caché. Búsqueda contra el servidor

El tope del coordinador: si la descarga inicial pasa de unas 5,000 filas, se baja la ventana de los últimos doce meses y el resto se consulta en línea.

Y una regla que va en la interfaz, no en el código: se avisa en pantalla qué está descargado y qué no. Un usuario que cree tener todo y no lo tiene es peor que uno que sabe que le falta, porque el primero va a concluir que el registro que busca no existe.

Lo que sí es igual para todos

La captura siempre es offline. Para los tres roles, sin excepción.

Ese es el punto entero de la aplicación: dar de alta a alguien de pie en la puerta de su casa, en una colonia sin señal, y que suba después. Si eso falla, la app no sirve, aunque la consulta funcione de maravilla.

Es una distinción útil en cualquier app de campo: leer puede degradarse, escribir no. Un listado que dice «no pude actualizar, esto es de hace dos horas» es aceptable. Un formulario que pierde lo que alguien acaba de teclear, no.

La forma de la base local

SQLite con expo-sqlite, una tabla por colección y una tabla de control:

territorio.db
├─ promovidos       espejo de lo visible + lo creado en campo
├─ promotores       espejo de promotores
├─ solicitudes      con seguimientos y verificaciones anidados como JSON
├─ verificaciones   capturadas en el dispositivo
├─ catalogos        los 19 catálogos y la geografía, en una sola tabla
├─ sync_outbox      la cola de salida
└─ kv               relojes, cursores y firmas

Dos decisiones ahí que ahorran trabajo.

Los catálogos van todos en una sola tabla, con el mismo nombre que usa la API. Diecinueve catálogos son diecinueve tablas de tres columnas que no se consultan nunca por separado; una tabla con una columna catalogo y un JSON hace lo mismo y la sincronización es una sola.

Lo anidado que sólo se lee se guarda como JSON. Los seguimientos de una solicitud llegan dentro de ella y no se editan desde el móvil. Normalizarlos en su propia tabla sería trabajo para nada: se guardan como vienen.

Las columnas de control

Cada tabla que se sincroniza lleva las mismas seis columnas. Esto es lo que hace que el motor de sincronización sea uno solo y no uno por colección:

Columna Para qué
uuid La llave que generó el teléfono. Es la primaria local y la que viaja
id El identificador del servidor. NULL hasta que sube por primera vez
sync_estado sincronizado · pendiente · enviando · rechazado
actualizado_en Cuándo lo tocó el servidor
actualizado_local Última edición en el dispositivo
rechazo_json Los errores campo por campo cuando el servidor lo rechazó

El uuid como llave primaria local es la decisión clave. El dispositivo crea un registro sin conexión; no puede pedirle un id al servidor porque no hay servidor. Necesita un identificador propio desde el instante cero, y necesita que ese identificador siga siendo válido después de subir.

Lo que compra es idempotencia: una subida que se corta a la mitad se reintenta completa y no duplica, porque el servidor reconoce el uuid que ya insertó. Sin eso, cada colonia con mala señal genera registros repetidos.

El id del servidor se guarda cuando llega, pero el uuid es el que manda al resolver a qué registro apunta una captura.

Dos columnas de fecha, no una

actualizado_en y actualizado_local parecen redundantes hasta que hay que decidir qué mostrar.

La primera dice cuándo el servidor tocó la fila. La segunda, cuándo la tocó el teléfono. Con las dos puedes contestar «esto que ves lo editaste tú hace diez minutos y todavía no sube», que es información que el usuario necesita y que con una sola columna es imposible dar.

Cuándo se dispara

Tres momentos: al abrir la app, al recuperar conexión —NetInfo avisa— y con un tirón manual hacia abajo.

El tirón manual no es decorativo. En campo, la gente sabe cuándo tiene señal —acaba de subir a la avenida— y quiere sincronizar en ese momento, no cuando a la app le parezca.

Lo que no intenté resolver

Sincronización bidireccional con resolución automática de conflictos. El servidor de esta plataforma no compara versiones: la última escritura queda, y no hay respuesta de conflicto.

Podría haber simulado una detección del lado del móvil, guardando la versión base y comparando. Decidí no hacerlo: una detección de conflictos que el otro lado no respalda es teatro, y da una confianza que no corresponde a lo que realmente pasa con los datos.

Es más honesto documentar que gana la última escritura, diseñar el reparto de trabajo para que dos personas no editen lo mismo, y gastar ese esfuerzo en que la cola de salida sea impecable.

Si estás por decidirlo

La pregunta con la que empezaría, antes que cualquier otra sobre bibliotecas:

¿Cuántos registros tiene a su alcance el usuario más común de la app? No el total de la base. El del usuario que va a abrirla veinte veces al día.

Si son cientos, offline completo y no lo pienses más. Si son cientos de miles, caché de lo que abre y búsqueda en línea. Y si la respuesta cambia mucho entre un rol y otro —que es lo normal—, entonces la arquitectura tiene que cambiar con ella, en lugar de aplicarle a todos el modo del caso más difícil.

#arquitectura #react-native #expo #sqlite #offline
Comentarios 0

Nadie ha comentado todavía. Estrena la sección.

Deja tu comentario
Tu correo no se publica. Reviso los comentarios antes de publicarlos.

Seguir leyendo

móvil

Leer la credencial con OCR para React Native

El plan era leer el PDF417 del reverso: dato estructurado, determinista, sin ambigüedad. Los modelos nuevos lo sustituyeron por un QR cifrado. Todo tiene que salir de OCR, y eso cambia el diseño entero.

Administrador · 7 min