Sincronización en segundo plano para apps móviles "offline-first": conflictos, reintentos y UX
Planifica la sincronización en segundo plano de apps móviles "offline-first" con reglas claras de conflicto, lógica de reintentos y una UX sencilla para cambios pendientes en apps nativas Kotlin y SwiftUI.

El problema: los usuarios editan sin conexión y la realidad cambia
Alguien empieza una tarea con buena conexión, luego entra en un ascensor, en un rincón del almacén o en un túnel de metro. La app sigue funcionando, así que la persona sigue trabajando. Pulsa Guardar, añade una nota, cambia un estado, quizá incluso crea un registro nuevo. Todo parece bien porque la pantalla se actualiza al instante.
Más tarde, la conexión vuelve y la app intenta ponerse al día en segundo plano. Ahí es cuando la sincronización en segundo plano puede sorprender.
Si la app no es cuidadosa, la misma acción puede enviarse dos veces (duplicados), o un cambio más reciente en el servidor puede sobrescribir lo que el usuario acaba de hacer (ediciones perdidas). A veces la app muestra estados confusos como “Guardado” y “No guardado” al mismo tiempo, o un registro aparece, desaparece y vuelve a aparecer después de la sincronización.
Un conflicto es simple: se hicieron dos cambios diferentes sobre lo mismo antes de que la app pudiera reconciliarlos. Por ejemplo, un agente de soporte cambia la prioridad de un ticket a Alta mientras está offline, pero un compañero en línea cierra el ticket. Cuando el teléfono offline se reconecta, ambos cambios no pueden aplicarse limpiamente sin una regla.
El objetivo no es que el modo offline sea perfecto. El objetivo es que sea predecible:
- La gente puede seguir trabajando sin temor a perder su trabajo.
- La sincronización ocurre después sin duplicados misteriosos.
- Cuando algo necesita atención, la app deja claro qué pasó y qué hacer a continuación.
Esto es cierto tanto si programas a mano en Kotlin/SwiftUI como si construyes apps nativas con una plataforma sin código como AppMaster. La parte difícil no son los widgets de UI. Es decidir cómo se comporta la app cuando el mundo cambia mientras el usuario está offline.
Un modelo offline-first sencillo (sin jerga)
Una app offline-first asume que el teléfono perderá red a veces, pero la app debe seguir siendo utilizable. Las pantallas deben cargar y los botones deben funcionar incluso cuando el servidor no está disponible.
Cuatro términos cubren la mayor parte:
- Caché local: datos almacenados en el dispositivo para que la app muestre algo al instante.
- Cola de sincronización: lista de acciones que el usuario realizó mientras estaba offline (o con la red intermitente).
- Verdad del servidor: la versión almacenada en el backend que todos comparten eventualmente.
- Conflicto: cuando el cambio en cola del usuario ya no se puede aplicar limpiamente porque la versión del servidor cambió.
Un modelo mental útil es separar lecturas de escrituras.
Las lecturas suelen ser sencillas: muestra los mejores datos disponibles (a menudo desde la caché local) y luego actualiza silenciosamente cuando vuelve la red.
Las escrituras son diferentes. No confíes en “guardar todo el registro” de una sola vez. Eso falla en cuanto estás offline.
En su lugar, registra lo que hizo el usuario como pequeñas entradas en un registro de cambios. Por ejemplo: “poner estado en Aprobado”, “añadir comentario X”, “cambiar cantidad de 2 a 3”. Cada entrada va a la cola de sincronización con una marca de tiempo y un ID. La sincronización en segundo plano luego intenta entregarla.
El usuario sigue trabajando mientras los cambios pasan de pendientes a sincronizados.
Si usas una plataforma sin código como AppMaster, aun así quieres los mismos bloques: lecturas en caché para pantallas rápidas y una cola clara de acciones de usuario que pueda reintentarse, combinarse o marcarse cuando ocurra un conflicto.
Decide qué necesita realmente soporte offline
Offline-first puede sonar a “todo funciona sin conexión”, pero esa promesa es donde muchas apps se complican. Elige las partes que realmente se benefician del modo offline y deja el resto claramente online-only.
Piensa en términos de intención del usuario: ¿qué necesitan hacer las personas en un sótano, en un avión o en un almacén con conexión inestable? Un buen valor por defecto es soportar las acciones que crean o actualizan el trabajo cotidiano, y bloquear las acciones donde la “verdad más reciente” importa.
Un conjunto práctico de acciones offline suele incluir crear y editar registros principales (notas, tareas, inspecciones, tickets), redactar comentarios y adjuntar fotos (almacenadas localmente y subidas después). El borrado también puede funcionar, pero es más seguro hacerlo como borrado suave con una ventana de deshacer hasta que el servidor lo confirme.
Ahora decide qué debe permanecer en tiempo real porque el riesgo es demasiado alto. Pagos, cambios de permisos, aprobaciones y todo lo que implique datos sensibles normalmente deberías requerir conexión. Si el usuario no puede estar seguro de que la acción es válida sin comprobar el servidor, no la permitas offline. Muestra un mensaje claro de “requiere conexión”, no un error misterioso.
Define expectativas de frescura. “Offline” no es binario. Define cuánto puede estar desactualizado un dato: minutos, horas o “la próxima vez que se abra la app”. Pon esa regla en la interfaz con palabras claras, como “Última actualización hace 2 horas” y “Sincronizando cuando haya conexión”.
Finalmente, marca los datos con alto riesgo de conflicto desde el principio. Los recuentos de inventario, tareas compartidas y los mensajes de equipo son imanes de conflicto porque varias personas los editan rápido. Para esos casos, considera limitar las ediciones offline a borradores o capturar cambios como eventos separados en vez de sobrescribir un único valor.
Si construyes en AppMaster, este paso de decisión te ayuda a modelar datos y reglas de negocio para que la app pueda guardar borradores seguros offline mientras mantiene las acciones de riesgo solo en línea.
Diseña la cola de sincronización: qué guardas por cada cambio
Cuando un usuario trabaja offline, no intentes “sincronizar la base de datos”. Sincroniza las acciones del usuario. Una cola de acciones clara es la columna vertebral de la sincronización en segundo plano y se mantiene comprensible cuando algo sale mal.
Mantén las acciones pequeñas y humanas, alineadas con lo que realmente hizo el usuario:
- Crear un registro
- Actualizar campo(s) específicos
- Cambiar estado (enviar, aprobar, archivar)
- Borrar (preferiblemente borrado suave hasta confirmación)
Las acciones pequeñas son más fáciles de depurar. Si soporte necesita ayudar a un usuario, es mucho más sencillo leer “Cambió estado Borrador -> Enviado” que inspeccionar un gran blob de JSON cambiado.
Para cada acción en cola, guarda metadatos suficientes para reproducirla con seguridad y detectar conflictos:
- Identificador del registro (y un ID local temporal para registros nuevos)
- Marca de tiempo de la acción e identificador del dispositivo
- Versión esperada (o la última hora de actualización conocida) del registro
- Payload (los campos específicos cambiados, además del valor antiguo si puedes)
- Clave de idempotencia (un ID único de la acción para que los reintentos no creen duplicados)
Esa versión esperada es la clave para manejar conflictos con honestidad. Si la versión del servidor ha avanzado, puedes pausar y pedir una decisión en vez de sobrescribir silenciosamente a otra persona.
Algunas acciones deben aplicarse juntas porque el usuario las ve como un solo paso. Por ejemplo, “Crear pedido” más “Añadir tres líneas” debe tener éxito o fallar como unidad. Guarda un ID de grupo (o ID de transacción) para que el motor de sincronización pueda enviarlas juntas y confirmar todas o mantenerlas todas pendientes.
Ya sea que construyas a mano o en AppMaster, el objetivo es el mismo: cada cambio se registra una vez, se reproduce de forma segura y se puede explicar cuando algo no coincide.
Reglas de resolución de conflictos que puedes explicar a los usuarios
Los conflictos son normales. El objetivo no es hacerlos imposibles. El objetivo es hacerlos raros, seguros y fáciles de explicar cuando ocurren.
Nombra el momento en que ocurre un conflicto: la app envía un cambio y el servidor dice, “Ese registro no es la versión que empezaste a editar.” Por eso importa la versionado.
Mantén dos valores con cada registro:
- Versión del servidor (la versión actual en el servidor)
- Versión esperada (la versión que el teléfono pensó que estaba editando)
Si la versión esperada coincide, acepta la actualización y sube la versión del servidor. Si no coincide, aplica tu regla de conflicto.
Elige una regla por tipo de dato (no una regla para todo)
Diferentes datos necesitan reglas diferentes. Un campo de estado no es lo mismo que una nota larga.
Reglas que los usuarios suelen entender:
- Gana la última escritura: adecuado para campos de bajo riesgo, como una preferencia de vista.
- Combinar campos: lo mejor cuando los campos son independientes (estado vs notas).
- Preguntar al usuario: ideal para ediciones de alto riesgo como precio, permisos o totales.
- Gana el servidor y guarda una copia: conserva el valor del servidor, pero guarda la edición del usuario como borrador que pueda volver a aplicarse.
En AppMaster, estas reglas encajan bien en lógica visual: comprueba versiones, compara campos y luego elige el camino.
Decide cómo se comportan los borrados (o perderás datos)
Los borrados son el caso complicado. Usa una tumba (un marcador “eliminado”) en lugar de quitar el registro inmediatamente. Luego decide qué pasa si alguien edita un registro que fue borrado en otro lugar.
Una regla clara es: “Ganan los borrados, pero se puede restaurar.” Ejemplo: un comercial edita una nota de cliente offline, mientras un administrador borra ese cliente. Cuando la sincronización corre, la app muestra “El cliente fue eliminado. ¿Restaurar para aplicar tu nota?” Esto evita pérdidas silenciosas y mantiene el control con el usuario.
Reintentos y estados de fallo: mantenlo predecible
Cuando la sincronización falla, a la mayoría de usuarios no les importa por qué. Les importa si su trabajo está a salvo y qué pasará después. Un conjunto predecible de estados previene pánicos y tickets de soporte.
Empieza con un modelo de estado pequeño y visible y mantenlo consistente en las pantallas:
- En cola: guardado en el dispositivo, esperando red
- Sincronizando: enviando ahora
- Enviado: confirmado por el servidor
- Fallido: no se pudo enviar, se reintentará o necesita atención
- Necesita revisión: enviado, pero el servidor lo rechazó o lo marcó
Los reintentos deben ser respetuosos con la batería y datos. Usa reintentos rápidos al principio (para manejar cortes breves), luego desacelera. Un backoff simple como 1 min, 5 min, 15 min y luego cada hora es fácil de razonar. También reintenta solo cuando tenga sentido (no sigas reintentando un cambio inválido).
Trata los errores de forma diferente, porque la siguiente acción cambia:
- Offline / sin red: permanece en cola, reintenta cuando haya conexión
- Timeout / servidor no disponible: marcar como fallido, reintentar automáticamente con backoff
- Autenticación expirada: pausar la sincronización y pedir al usuario que inicie sesión otra vez
- Validación fallida (datos inválidos): necesita revisión, muestra qué corregir
- Conflicto (registro cambiado): necesita revisión, aplica tus reglas de conflicto
La idempotencia es lo que hace que los reintentos sean seguros. Cada cambio debería tener un ID único de acción (a menudo un UUID) que se envía con la petición. Si la app reenvía el mismo cambio, el servidor debería reconocer el ID y devolver el mismo resultado en vez de crear duplicados.
Ejemplo: un técnico guarda un trabajo completado offline y entra en un ascensor. La app envía la actualización, hace timeout y reintenta después. Con un ID de acción, el segundo envío es inofensivo. Sin él, podrías crear eventos “completado” duplicados.
En AppMaster, trata estos estados y reglas como campos y lógica de primera clase en tu proceso de sincronización, para que tus apps nativas Kotlin y SwiftUI se comporten igual en todas partes.
UX para cambios pendientes: qué ve y qué puede hacer el usuario
La gente debe sentirse segura usando la app offline. Una buena UX de “cambios pendientes” es calmada y predecible: reconoce que el trabajo está guardado en el dispositivo y hace obvio el siguiente paso.
Un indicador sutil funciona mejor que un banner de advertencia. Por ejemplo, muestra un pequeño icono de “Sincronizando” en el encabezado o una discreta etiqueta “3 pendientes” en la pantalla donde se hacen las ediciones. Usa colores alarmantes solo para peligros reales (como “no se puede subir porque has cerrado sesión”).
Dale al usuario un lugar para entender qué está pasando. Una simple Bandeja de salida o pantalla de Cambios pendientes puede listar ítems con lenguaje claro como “Comentario añadido al Ticket 104” o “Foto de perfil actualizada.” Esa transparencia evita pánicos y reduce tickets de soporte.
Qué puede hacer el usuario
La mayoría de la gente solo necesita unas pocas acciones, y deben ser consistentes en toda la app:
- Reintentar ahora
- Volver a editar (crea un cambio más reciente)
- Descartar el cambio local
- Copiar detalles (útil para reportar un problema)
Mantén las etiquetas de estado simples: Pendiente, Sincronizando, Fallido. Cuando algo falla, explícalo como lo haría una persona: “No se pudo subir. Sin internet.” o “Rechazado porque este registro lo cambió otra persona.” Evita códigos de error.
No bloquees toda la app
Solo bloquea las acciones que realmente requieren estar en línea, como “Pagar con Stripe” o “Invitar a un nuevo usuario.” Todo lo demás debe seguir funcionando, incluida la visualización de datos recientes y la creación de borradores nuevos.
Un flujo realista: un técnico de campo edita un informe de trabajo en un sótano. La app muestra “1 pendiente” y le deja seguir trabajando. Más tarde, cambia a “Sincronizando” y luego se borra automáticamente. Si falla, el informe queda disponible, marcado como “Fallido”, con un solo botón “Reintentar ahora”.
Si construyes en AppMaster, modela estos estados como parte de cada registro (pendiente, fallido, sincronizado) para que la UI pueda reflejarlos en todas partes sin pantallas especiales.
Autenticación, permisos y seguridad mientras estás offline
El modo offline cambia tu modelo de seguridad. Un usuario puede hacer acciones sin conexión, pero tu servidor sigue siendo la fuente de la verdad. Trata cada cambio en cola como “solicitado”, no como “aprobado”.
Caducidad de sesión mientras estás offline
Los tokens caducan. Cuando eso pasa sin conexión, deja que el usuario siga creando ediciones y guárdalas como pendientes. No finjas que las acciones que requieren confirmación del servidor están terminadas. Márquelas como pendientes hasta la siguiente renovación de sesión exitosa.
Cuando la app vuelva a línea, intenta primero una renovación silenciosa. Si debes pedir al usuario que inicie sesión de nuevo, hazlo una sola vez y luego reanuda la sincronización automáticamente.
Tras el reingreso, revalida cada ítem en cola antes de enviarlo. La identidad del usuario puede haber cambiado (dispositivo compartido) y las ediciones antiguas no deben sincronizarse bajo la cuenta equivocada.
Cambios de permisos y acciones prohibidas
Los permisos pueden cambiar mientras el usuario está offline. Una edición que ayer estaba permitida puede estar prohibida hoy. Maneja esto explícitamente:
- Revisa permisos server-side para cada acción en cola
- Si está prohibida, detén ese ítem y muestra una razón clara
- Conserva la edición local para que el usuario la copie o solicite acceso
- Evita reintentos repetidos para errores de “prohibido”
Ejemplo: un agente de soporte edita una nota de cliente offline en un vuelo. Durante la noche le quitan su rol. Cuando la sincronización corre, el servidor rechaza la actualización. La app debe mostrar “No se puede subir: ya no tienes acceso” y mantener la nota como borrador local.
Datos sensibles almacenados offline
Almacena lo mínimo necesario para renderizar pantallas y reproducir la cola. Encripta el almacenamiento offline, evita cachéar secretos y define reglas claras para cierre de sesión (por ejemplo: borrar datos locales o conservar borradores solo con consentimiento explícito). Si construyes con AppMaster, empieza con su módulo de autenticación y diseña tu cola para que siempre espere una sesión válida antes de enviar cambios.
Trampas comunes que causan pérdida de trabajo o registros duplicados
La mayoría de los bugs offline no son sofisticados. Vienen de unas pocas decisiones pequeñas que parecen inocuas cuando pruebas con Wi‑Fi perfecto, pero rompen el trabajo real más tarde.
Un fallo común son las sobreescrituras silenciosas. Si la app sube una versión antigua y el servidor la acepta sin comprobar, puedes borrar la edición más reciente de otra persona y nadie se da cuenta hasta que ya es tarde. Sincroniza con un número de versión (o sello "updatedAt") y rehúsa sobrescribir cuando el servidor haya avanzado, así el usuario obtiene una elección clara.
Otra trampa es la tormenta de reintentos. Cuando un teléfono recupera una conexión débil, la app puede bombardear el backend cada pocos segundos, consumiendo batería y generando escrituras duplicadas. Los reintentos deben ser tranquilos: desacelera tras cada fallo y añade un poco de aleatoriedad para que miles de dispositivos no reintenten al mismo tiempo.
Los errores que más a menudo llevan a pérdida de trabajo o duplicados:
- Tratar todo fallo como “red”: separa errores permanentes (datos inválidos, permiso faltante) de temporales (timeout).
- Ocultar fallos de sincronización: si la gente no puede ver qué falló, rehacen la tarea y crean dos registros.
- Enviar el mismo cambio dos veces sin protección: siempre adjunta un ID de petición único para que el servidor reconozca e ignore duplicados.
- Fusionar campos de texto automáticamente sin avisar: si combinas ediciones, deja que los usuarios revisen el resultado cuando importe.
- Crear registros offline sin un ID estable: usa un ID local temporal y mapea al ID del servidor tras la subida, para que ediciones posteriores no creen una segunda copia.
Un ejemplo rápido: un técnico de campo crea una nueva “Visita al sitio” offline y luego la edita dos veces antes de reconectar. Si la llamada de creación se reintenta y crea dos registros en el servidor, las ediciones posteriores pueden adjuntarse al equivocado. IDs estables y deduplicación server-side previenen esto.
Si construyes esto con AppMaster, las reglas no cambian. La diferencia es dónde las implementas: en la lógica de sincronización, en el modelo de datos y en las pantallas que muestran “fallado” vs “enviado”.
Escenario de ejemplo: dos personas editan el mismo registro
Una técnica de campo, Maya, está actualizando el ticket “Job #1842” en un sótano sin señal. Cambia el estado de “En curso” a “Completado” y añade una nota: “Reemplacé la válvula, probado OK.” La app guarda al instante y lo muestra como pendiente.
Arriba, su compañero Leo está en línea y edita el mismo trabajo al mismo tiempo. Cambia la hora programada y asigna el trabajo a otro técnico, porque un cliente llamó con una actualización.
Cuando Maya recupera señal, la sincronización en segundo plano empieza silenciosamente. Esto es lo que ocurre en un flujo predecible y amigable:
- El cambio de Maya sigue en la cola (ID del trabajo, campos cambiados, marca de tiempo y la versión del registro que vio).
- La app intenta subirlo. El servidor responde: “Este trabajo fue actualizado desde tu versión” (conflicto).
- Se ejecuta la regla de conflicto: el estado y las notas se pueden fusionar, pero los cambios de asignación ganan si se hicieron después en el servidor.
- El servidor acepta un resultado combinado: estado = “Completado” (de Maya), nota añadida (de Maya), técnico asignado = elección de Leo (de Leo).
- El trabajo se vuelve a abrir en la app de Maya con un banner claro: “Sincronizado con actualizaciones. La asignación cambió mientras estabas offline.” Una pequeña acción “Revisar” muestra qué cambió.
Ahora añade un fallo: el token de sesión de Maya caducó mientras estaba offline. El primer intento de sincronización falla con “Se requiere inicio de sesión”. La app mantiene sus ediciones, las marca como “Pausadas” y muestra un único aviso. Tras iniciar sesión, la sincronización se reanuda automáticamente sin que tenga que reescribir nada.
Si hay un problema de validación (por ejemplo, “Completado” requiere una foto), la app no debe adivinar. Marca el ítem como “Necesita atención”, le dice exactamente qué añadir y luego le permite reenviarlo.
Plataformas como AppMaster pueden ayudar aquí porque puedes diseñar la cola, las reglas de conflicto y la UX de pendientes de forma visual, mientras sigues distribuyendo apps nativas reales en Kotlin y SwiftUI.
Lista rápida de comprobación y siguientes pasos
Trata la sincronización offline como una funcionalidad de extremo a extremo que puedes probar, no como un montón de parches. El objetivo es simple: los usuarios nunca se preguntan si su trabajo está guardado y la app no crea duplicados sorpresa.
Una lista corta para confirmar que la base es sólida:
- La cola de sincronización está almacenada en el dispositivo y cada cambio tiene un ID local estable más un ID del servidor cuando está disponible.
- Existen estados claros (en cola, sincronizando, enviado, fallido, necesita revisión) y se usan de forma consistente.
- Las solicitudes son idempotentes (seguras para reintentar) y cada operación incluye una clave de idempotencia.
- Los registros tienen versionado (updatedAt, número de revisión o ETag) para detectar conflictos.
- Las reglas de conflicto están escritas en lenguaje claro (qué gana, qué se fusiona, cuándo preguntas al usuario).
Una vez en su lugar, verifica que la experiencia sea tan sólida como el modelo de datos. Los usuarios deben poder ver qué está pendiente, entender qué falló y actuar sin miedo a perder trabajo.
Prueba con escenarios que coincidan con la vida real:
- Ediciones en modo avión: crear, actualizar, borrar y luego reconectar.
- Red inestable: perder la conexión a mitad de la sincronización y comprobar que los reintentos no duplican.
- App cerrada: forzar el cierre durante el envío, reabrir y confirmar que la cola se recupera.
- Desajuste de hora: el tiempo del dispositivo está mal, confirmar que la detección de conflictos sigue funcionando.
- Pulsaciones duplicadas: el usuario pulsa Guardar dos veces, confirmar que se convierte en un solo cambio en el servidor.
Protoipa el flujo completo antes de pulir la UI. Construye una pantalla, un tipo de registro y un caso de conflicto (dos ediciones al mismo campo). Añade un área simple de estado de sincronización, un botón Reintentar para fallos y una pantalla clara de conflicto. Cuando eso funcione, repite para más pantallas.
Si construyes sin código, AppMaster (appmaster.io) puede generar apps nativas Kotlin y SwiftUI junto al backend, para que te centres en la cola, las comprobaciones de versión y los estados visibles al usuario en lugar de conectar todo a mano.


