Una aplicación .NET MAUI offline no se limita a avisar de que no hay conexión. Debe permitir consultar información local y, cuando el proceso lo admita, registrar cambios en el dispositivo para sincronizarlos después con el servidor. Para conseguirlo hacen falta una base de datos local, una cola persistente de operaciones, una API preparada para reintentos y una política explícita ante conflictos.
.NET MAUI permite compartir gran parte de la interfaz y la lógica entre Android, iOS, macOS y Windows. Sin embargo, no incorpora una receta universal que resuelva por sí sola toda la sincronización offline. SQLite, Connectivity, el ciclo de vida de la app y SecureStorage son piezas disponibles; el protocolo de sincronización debe diseñarse según los datos y las reglas del negocio.
Antes de valorar la arquitectura, también conviene revisar si la tecnología encaja con el proyecto. Puedes ampliar ese análisis en ¿Sigue mereciendo la pena .NET MAUI en 2026?.
Qué significa realmente que una app funcione offline
Un enfoque offline-first parte de que la fuente inmediata de lectura es el dispositivo. La aplicación muestra los datos que ya tiene disponibles y no bloquea necesariamente al usuario por la ausencia de red. Cuando vuelve a poder comunicarse con el backend, intenta intercambiar los cambios pendientes.
Conviene clasificar la información antes de definir el alcance:
- Datos de referencia: catálogos, listas o fichas que pueden descargarse previamente para consultarse sin conexión.
- Datos creados o editados por el usuario: por ejemplo, una visita, un pedido o una incidencia que puede guardarse localmente y enviarse después.
- Operaciones con validación inmediata: acciones que dependen de permisos, stock, aprobaciones u otras reglas que solo el servidor puede confirmar en ese momento.
El modo offline no elimina el backend. La autenticación, los permisos, las validaciones, la auditoría y la consolidación de cambios siguen requiriendo una estrategia de servidor. Tampoco garantiza que todos los datos locales sobrevivan a una desinstalación, corrupción de la base o pérdida del dispositivo.
Arquitectura mínima para sincronizar sin conexión
Separar responsabilidades evita que la interfaz de usuario termine controlando directamente la red, la base local y los reintentos. Una arquitectura práctica puede incluir estas capas:
| Componente | Responsabilidad |
|---|---|
| Interfaz y lógica de presentación | Muestra datos locales, informa de cambios pendientes y permite actuar al usuario. |
| Repositorio local | Consulta y actualiza la base SQLite del dispositivo. |
| Cola de operaciones u outbox | Conserva de forma persistente altas, cambios y borrados pendientes de enviar. |
| Cliente de API | Realiza solicitudes al backend, interpreta respuestas y comunica errores. |
| Motor de sincronización | Coordina la descarga de cambios, el envío de operaciones, los reintentos y los conflictos. |
Esta separación permite probar cada parte por separado y distinguir un error de interfaz de una operación que quedó pendiente por falta de respuesta del servidor.
Almacenamiento local con SQLite
SQLite es una opción documentada para almacenar y consultar datos locales desde código compartido de .NET MAUI. Conviene usar un directorio persistente de datos de la aplicación, como FileSystem.AppDataDirectory, en lugar de una ubicación temporal.
La documentación de .NET MAUI usa sqlite-net-pcl en su ejemplo y menciona Microsoft.Data.Sqlite como alternativa ADO.NET ligera. No hay una elección automática: conviene valorar la compatibilidad con la versión del proyecto, el tipo de consultas, las migraciones de esquema, el mantenimiento y las pruebas necesarias.
Modelo local orientativo
Además de las tablas de negocio, una aplicación offline-first suele necesitar metadatos para conocer el estado de cada registro y una cola persistente. El siguiente esquema es conceptual; no lo impone .NET MAUI.
| Elemento local | Contenido habitual | Finalidad |
|---|---|---|
| Entidad de negocio | Identificador, campos funcionales, versión o marca recibida del servidor y estado local. | Permitir lectura y edición sin conexión. |
| Operación pendiente | Identificador de operación, entidad, identificador del registro, tipo de acción, fecha, estado e intentos. | Reanudar el envío sin perder cambios ni depender de la memoria de la app. |
| Estado de sincronización | Cursor, versión o marca entregada por el servidor. | Solicitar cambios incrementales en lugar de descargar todo en cada intento. |
| Errores de sincronización | Operación afectada, error recibido, fecha y detalle técnico controlado. | Diagnosticar incidencias y decidir si se reintenta o se requiere intervención. |
Al guardar una modificación offline, es recomendable persistir la actualización de la entidad y la operación pendiente dentro de una transacción. Así se reduce el riesgo de que la interfaz muestre un cambio que no tiene una acción asociada para enviarse más adelante.
Ejemplo mínimo: guardar el dato y la operación pendiente
El siguiente pseudocódigo ilustra el objetivo de la transacción. La API, el modelo de errores y la biblioteca de acceso a SQLite pueden cambiar según el proyecto.
async Task GuardarPedidoOfflineAsync(Pedido pedido)
{
await database.RunInTransactionAsync(connection =>
{
connection.InsertOrReplace(pedido);
connection.Insert(new PendingOperation
{
OperationId = Guid.NewGuid().ToString(),
Entity = "Pedido",
RecordId = pedido.Id,
Action = "upsert",
ExpectedVersion = pedido.ServerVersion,
Status = "pending",
Attempts = 0
});
});
}
La finalidad no es imponer una implementación concreta, sino evitar estados parciales: si se guarda el pedido, también debe quedar registrada la operación necesaria para enviarlo al backend.
SQLite ofrece Write-Ahead Logging (WAL) como configuración avanzada. Puede facilitar lecturas y escrituras concurrentes con menos bloqueo, pero añade archivos auxiliares y requisitos de checkpoint. Debe evaluarse con pruebas de concurrencia, cierre y recuperación antes de activarlo.
SQLite no es SecureStorage
La base SQLite sirve para datos funcionales y caché local. Los tokens y otros valores pequeños sensibles deben tratarse de forma distinta. .NET MAUI proporciona SecureStorage para pares clave-valor simples, aunque el equipo debe contemplar diferencias entre plataformas, copias de seguridad, restauraciones y posibles errores al recuperar valores cifrados. No está pensado para almacenar grandes volúmenes de texto ni para sustituir una base de datos local.
Cuando se manejan datos personales o información empresarial sensible, el diseño debe revisar minimización de datos, permisos, cifrado, copias de seguridad y almacenamiento de credenciales. Las obligaciones aplicables deben validarse con los perfiles técnicos y legales adecuados.
Flujo de lectura y escritura offline
Lectura: local primero, actualización después
- La pantalla consulta SQLite y muestra los datos disponibles.
- Si procede, el motor intenta contactar con la API para solicitar cambios desde el último cursor, versión o marca confirmada por el servidor.
- Si la respuesta es válida, actualiza la base local en una transacción.
- La interfaz se refresca a partir del repositorio local, no directamente desde la respuesta de red.
La descarga debe definir paginación, orden y tratamiento de eliminaciones. Para datos que desaparecen del servidor, un borrado lógico puede ser más seguro que asumir que la ausencia en una página equivale a una eliminación.
Escritura: guardar, encolar y sincronizar
- El usuario crea, modifica o elimina un dato.
- La app actualiza SQLite y registra una operación pendiente en la misma transacción.
- La interfaz refleja el cambio local e indica, si es relevante, que está pendiente de sincronización.
- El motor intenta enviar la operación cuando puede comunicarse realmente con el backend.
- Tras una confirmación válida, marca la operación como completada y actualiza las versiones o marcas recibidas.
Ejemplo conceptual de una operación pendiente
Operación pendiente
- operationId: "8c4f..."
- entity: "Pedido"
- recordId: "local-123"
- action: "create"
- expectedVersion: null
- status: "pending"
- attempts: 0
- createdAt: fecha local de creación
Un ciclo de estados posible sería pending, sending, confirmed, retryable-error y conflict. Si la app se cierra mientras una operación está en sending, al reabrirse no debería asumir que el servidor no la procesó: debe revisarla o reenviarla mediante una clave de idempotencia.
La sincronización debe concretar si primero descarga cambios, si primero sube los locales o si alterna fases. La decisión depende de las dependencias entre entidades y de las reglas del backend.
Connectivity es una señal, no una prueba de que la API responde
La API Connectivity de .NET MAUI permite consultar NetworkAccess, revisar perfiles de conexión y reaccionar al evento ConnectivityChanged. Sus estados incluyen Internet, ConstrainedInternet, Local, None y Unknown.
Puede utilizarse como señal para lanzar un intento de sincronización, pero no como garantía de que el endpoint concreto esté disponible. Un portal cautivo, un router con problemas o una incidencia del servicio remoto pueden hacer que una petición falle aunque el dispositivo informe de acceso a Internet.
Por tanto, el cliente de API debe comprobar la petición real y gestionar timeouts, errores HTTP, respuestas parciales y fallos transitorios. Un patrón de diseño habitual incluye reintentos progresivos, límites de intentos, registro de errores y una cola que pueda continuar en una ejecución posterior.
No marques una operación como sincronizada solo porque haya conectividad. Márcala como completada cuando el backend haya confirmado una respuesta que el protocolo considere válida.
Diseñar una sincronización incremental, reanudable e idempotente
Una app que se conecta de forma irregular necesita poder interrumpirse y continuar sin duplicar acciones. Estas decisiones deben formar parte del contrato de la API, no quedarse únicamente en la aplicación móvil.
Usa cursores, versiones o marcas del servidor
Para descargar cambios incrementales, el servidor debería devolver un cursor, una versión o una marca de modificación que la app guarde al completar cada bloque. Confiar solo en DateTime del dispositivo puede causar omisiones u órdenes incorrectos si el reloj local está desajustado.
La API debe definir además cómo pagina resultados y cuándo avanza el cursor. Si la descarga se interrumpe, la app debe poder repetir el bloque sin perder cambios ni aplicar resultados incompletos.
Evita duplicados con identificadores de operación
Una solicitud puede llegar al servidor y perderse la respuesta antes de que la app la reciba. Si se reintenta sin más, puede crear un duplicado. Para evitarlo, cada operación pendiente puede llevar un identificador único o una clave de idempotencia que el backend reconozca.
POST /pedidos
Idempotency-Key: 8c4f...
{
"clientRecordId": "local-123",
"clienteId": "cli-42",
"expectedVersion": null
}
Si el backend recibe de nuevo la misma clave, debe devolver un resultado coherente con la operación ya procesada en vez de crear una segunda alta. Este comportamiento requiere diseño y pruebas de ambos extremos.
Procesar una operación pendiente
async Task ProcesarAsync(PendingOperation operation)
{
MarcarComoEnviando(operation);
var response = await api.SendAsync(operation);
if (response.IsSuccess)
ConfirmarOperacionYActualizarVersion(operation, response.Version);
else if (response.IsConflict)
MarcarComoConflicto(operation, response.ServerVersion);
else
ProgramarReintento(operation);
}
El ejemplo presupone que cada cambio se identifica de forma persistente y que el backend participa en la idempotencia y el control de versiones. Sin esas dos partes, reintentar una petición no evita por sí mismo los duplicados ni las sobrescrituras.
Contempla el orden y los borrados
Las operaciones tienen dependencias. Un alta puede necesitar confirmación antes de que se envíen modificaciones relacionadas; un borrado puede competir con una edición remota. Define qué operaciones pueden agruparse, cuáles deben respetar orden y cómo se representa un borrado local antes de que el servidor lo confirme.
Conflictos: qué hacer si el mismo dato cambia en dos sitios
Existe un conflicto cuando la versión sobre la que el usuario editó ya no coincide con la versión que el backend considera actual. La concurrencia optimista ofrece un patrón de referencia: comparar la versión original conocida con la versión almacenada y rechazar o tratar el cambio si otra operación modificó el registro.
Una app .NET MAUI con SQLite y una API REST no obtiene este control extremo a extremo automáticamente. La API debe aceptar una versión, token o equivalente, y responder de forma clara cuando detecte un cambio concurrente.
Respuesta conceptual de conflicto
HTTP 409 Conflict
{
"recordId": "pedido-42",
"serverVersion": "v18",
"reason": "El registro fue modificado por otra operación"
}
| Estrategia | Cuándo podría encajar | Riesgo |
|---|---|---|
| Prevalece el servidor | Cuando el backend es la fuente autorizada y no debe sobrescribirse automáticamente. | Puede descartar cambios locales legítimos si no se conservan para revisión. |
| Prevalece el cliente | Solo en datos donde la última edición local sea aceptable por la regla de negocio. | Puede sobrescribir información remota válida. |
| Combinación campo a campo | Cuando los campos son independientes y la semántica permite unirlos. | Exige reglas detalladas y puede generar combinaciones incoherentes. |
| Revisión manual | En información crítica, aprobaciones o datos con alto riesgo de error. | Introduce una tarea pendiente que debe tener responsable y experiencia de usuario definida. |
No hay una política universal. Debe acordarse con el responsable del proceso qué campos se pueden fusionar, quién resuelve un conflicto y cómo se informa al usuario. También conviene conservar trazabilidad suficiente para investigar una operación rechazada.
El ciclo de vida no puede ser el único desencadenante
Los eventos de ciclo de vida de .NET MAUI incluyen, entre otros, Activated, Deactivated, Stopped, Resumed y Destroying. Un evento como Resumed, igual que ConnectivityChanged, puede desencadenar un intento de sincronización. No debe ser la única garantía de que el trabajo terminará: el sistema operativo puede suspender o finalizar una app en segundo plano.
Como mínimo, la aplicación debe revisar y reanudar la cola al abrirse o reactivarse, sin asumir que el trabajo terminará en segundo plano. La sincronización automática en segundo plano requiere estudiar por separado las capacidades, permisos y restricciones de cada plataforma objetivo; no conviene asumir un comportamiento idéntico en Android, iOS, macOS y Windows.
La operación posterior también forma parte del diseño. Consulta Mantenimiento de aplicaciones .NET MAUI: publicación, responsabilidades y controles para considerar responsabilidades, pruebas y publicación tras el desarrollo inicial.
Pruebas que no deberían faltar
La sincronización offline no se valida solo comprobando que una pantalla se abra sin Wi-Fi. El plan de pruebas debería incluir, como mínimo:
- Pérdida de red durante una lectura, una escritura y una descarga paginada.
- Respuesta del servidor recibida parcialmente o tras superar un timeout.
- Reintento de una petición que el servidor pudo procesar antes de que se cortara la respuesta.
- Ediciones simultáneas del mismo registro desde otro dispositivo, un usuario o un sistema integrado.
- Reloj del dispositivo incorrecto.
- Cierre, suspensión o terminación de la app con operaciones pendientes.
- Reinstalación, actualización del esquema de SQLite y recuperación ante una base local dañada.
- Volúmenes de datos, dispositivos, permisos y condiciones de red representativos del uso real.
Si la aplicación procede de Xamarin.Forms, la sincronización offline es además un buen momento para revisar dependencias, almacenamiento local y comportamiento por plataforma. Puede ayudarte Xamarin.Forms vs .NET MAUI: diferencias clave antes de migrar.
Cuándo basta una solución estándar y cuándo hace falta diseño a medida
Una solución estándar puede ser suficiente si se trabaja con datos simples, un número reducido de entidades, conflictos poco frecuentes y reglas de sincronización limitadas. Aun así, hay que comprobar la compatibilidad con la versión concreta de .NET MAUI, el mantenimiento de las bibliotecas, las migraciones y el comportamiento en los dispositivos objetivo.
Una sincronización a medida suele tener más sentido cuando intervienen varios sistemas, permisos complejos, reglas de negocio específicas, trazabilidad, dependencias entre registros o conflictos habituales. En esos casos, el esfuerzo principal no está solo en la pantalla móvil: está en definir el contrato de la API, las versiones, los estados de operación, las validaciones y la recuperación de incidencias.
Antes de iniciar el proyecto, responde a estas preguntas:
- ¿Qué información debe poder consultarse y editarse realmente sin conexión?
- ¿Cuánto tiempo puede estar un usuario desconectado?
- ¿Qué ocurre si dos personas cambian el mismo registro?
- ¿Qué validaciones pueden esperar y cuáles exigen respuesta inmediata del servidor?
- ¿Qué datos no deberían persistir en el dispositivo?
- ¿Qué sistema será la fuente autorizada para cada entidad?
Las respuestas delimitan una aplicación offline viable y evitan prometer una sincronización aparentemente simple que después no respeta el proceso empresarial.
Preguntas frecuentes
¿Se puede crear una aplicación .NET MAUI que funcione sin conexión?
Sí. Una aplicación .NET MAUI puede consultar datos almacenados localmente y registrar cambios sin conexión si incorpora una base local, como SQLite, y una estrategia para sincronizar posteriormente con el backend. El alcance depende de qué datos y operaciones puedan realizarse sin validación inmediata del servidor.
¿SQLite es suficiente para una app .NET MAUI offline?
SQLite puede cubrir el almacenamiento y las consultas locales, pero no resuelve por sí sola la sincronización. También hay que diseñar una cola persistente de operaciones, la API, los reintentos, el control de versiones, la idempotencia y la resolución de conflictos.
¿Connectivity.NetworkAccess confirma que la API está disponible?
No. NetworkAccess sirve para conocer el estado de conectividad del dispositivo y decidir si conviene intentar sincronizar, pero no garantiza que el endpoint concreto responda. La aplicación debe realizar la petición real y gestionar timeouts, errores HTTP y reintentos.
¿Dónde se guardan los tokens y credenciales en una app .NET MAUI?
Los tokens y otros valores pequeños sensibles deben tratarse de forma distinta a los datos funcionales de SQLite. .NET MAUI ofrece SecureStorage para pares clave-valor simples, aunque el equipo debe revisar las diferencias entre plataformas, copias de seguridad, restauraciones y el tratamiento de posibles errores al recuperar valores cifrados.
¿Cómo se evitan duplicados al reintentar una sincronización?
Cada operación pendiente puede incluir un identificador único o una clave de idempotencia que el backend reconozca. Si la app repite una solicitud porque no recibió la respuesta anterior, el servidor debe identificarla como la misma operación y evitar crear un segundo registro.
¿Qué estrategia de conflicto conviene utilizar?
Depende del significado y del riesgo de cada dato. Puede prevalecer el servidor, prevalecer el cliente, combinar campos independientes o requerir revisión manual. La decisión debe validarse con el responsable del proceso, porque una regla automática puede sobrescribir información legítima.
¿Necesitas una aplicación .NET Maui Offline?
Estudiamos tu necesidad y creamos una aplicación .NET Maui adaptada para funcionar Offline según tus necesidades.


