Offline-first app: architettura

Offline-first app: architettura

Una app offline-first funziona anche senza connessione, salvando dati in locale e sincronizzando quando possibile. Per gestionali, mobility, sanita e logistica e un requisito di business. Realizzarla bene richiede scelte architetturali precise.

Storage locale

SQLite (con Room su Android, GRDB su iOS), Realm, ObjectBox, MMKV per chiavi-valore semplici. Il database locale e la fonte di verita immediata per l'utente. Le operazioni di lettura non devono mai dipendere dalla rete.

Sincronizzazione

Patterns: write-through (scrivi sia locale che remoto), write-behind (scrivi locale, sync asincrono), CRDT per multi-writer offline. Considera idempotenza (header Idempotency-Key), ordini di operazioni, retry con backoff esponenziale.

Conflict resolution

Ultimo vince (last-write-wins), merge campo per campo, version vector, CRDT, intervento utente. La scelta dipende dal dominio: per anagrafiche last-write-wins puo bastare; per documenti collaborativi servono CRDT o operazione transform.

Outbox pattern

Pattern: ogni operazione che richiede sync va salvata in una "outbox" table locale. Un worker periodico la legge, invia al server, aggiorna stato (pending, sent, ack, error). Idempotenza tramite Idempotency-Key. Garantisce che nessuna operazione utente venga persa in scenari di rete instabile.

CRDT in dettaglio

Conflict-free Replicated Data Types: strutture dati che convergono automaticamente. G-Set (insiemi grow-only), LWW-Register (last-write-wins con timestamp), OR-Set, RGA (per sequenze). Yjs e Automerge sono librerie mature. Adatti a documenti collaborativi (Notion-style), chat, kanban.

Background sync strategies

WorkManager Android: lavori postponibili con vincoli (network, charging, idle). BGAppRefreshTask iOS: finestra molto ridotta. Push silent: il server notifica al client che ci sono dati nuovi, l'app fa fetch al risveglio. Combinato con local notifications per UX migliorata.

UX dello stato sync

L'utente deve sempre sapere se i suoi dati sono sincronizzati. Icona di stato (cloud, cloud con check, cloud con freccia). Toast esplicativi su sync error. In caso di conflict, dialog chiaro con opzioni "tieni mia versione" / "tieni server" / "merge". Trasparenza aumenta fiducia.

Encryption at rest

Dati sensibili in storage locale devono essere cifrati. Su iOS, Keychain per chiavi e segreti, Data Protection per file system (con NSFileProtectionComplete). Android: EncryptedSharedPreferences, EncryptedFile da androidx.security.crypto. Per database, SQLCipher (cross-platform) cifra l'intero database con chiave master. Compliance GDPR e settori regolati lo richiedono.

Migrations e schema evolution

Schema del database locale evolve nel tempo. Pianifica migrations: aggiungere colonne sicuro, rinominare/rimuovere richiede script. Room (Android) e GRDB (iOS) supportano migrations dichiarative. Mai cancellare e ricreare in produzione: perdita dati utente. Test ogni migration su dataset reali esportati dai client.

Procedura passo-passo

  1. Definisci quali entita servono offline e quali solo online.
  2. Scegli uno storage locale solido (Room, GRDB, Realm).
  3. Imposta un layer di repository tra UI e dati.
  4. Marca ogni record con stato (synced, pending, conflict).
  5. Implementa una coda di operazioni con retry.
  6. Definisci la strategia di conflict resolution.
  7. Mostra all'utente lo stato (icone, badge "non sincronizzato").

Real-time sync con WebSocket

Per app collaborative, WebSocket persistenti gestiscono push real-time. Combina con outbox locale per resilienza offline. Stack tipico: locale repository -> sync service -> WebSocket + outbox queue. Server gestisce broadcast a tutti i client connessi nello stesso topic. Esempi: chat (Slack, Discord), collaboration (Figma, Notion), live dashboards.

Testing offline scenarios

Test esaustivamente: airplane mode, slow 3G, packet loss, disconnect mid-sync, conflitto write-write. iOS Simulator offre Network Link Conditioner; Android usa Charles Proxy con network throttling. Crea matrix di test scenarios e automatizzali. Bug offline-first emergono in casi edge non testabili manualmente.

Storage limits

iOS espone NSURL ubiquitousItemPercentDownloadedKey per gestire storage; Android offre File.getUsableSpace(). Imposta limite massimo per dati offline (es. 500 MB), implementa cleanup automatico di dati vecchi (LRU eviction). Senza, l'app puo arrivare a riempire lo storage utente, causando rimozione.

Errori comuni e come risolverli

  • Stato di sync invisibile: l'utente perde fiducia. Mostra sempre indicatori.
  • Mancata idempotenza: duplicati ad ogni retry.
  • Conflict ignorati: dati persi senza preavviso.
  • Tutto in memoria: dati persi a kill.
  • Encryption mancante: dati sensibili in chiaro su disco.

Domande frequenti

D: CRDT sono adatti a ogni app?
R: No, hanno overhead. Usali solo se hai collaborazione real-time multi-writer.

D: SQLite cifrato e sicuro?
R: Si con SQLCipher; integra con storage sicuro per la chiave (Keychain, Keystore).

D: Quanto storage e accettabile?
R: Dipende dal dispositivo; mantieni un meccanismo di cleanup automatico.

D: SQLite o NoSQL per offline-first?
R: Dipende dai dati. SQLite per dati strutturati con query complesse; document store (Realm, Couchbase) per documenti flessibili.

Conclusione strategica

Offline-first non e solo robustezza tecnica ma anche UX migliore: l'app risponde sempre istantaneamente, anche con rete instabile. Pianifica architettura dall'inizio. Refactor tardivo e doloroso e costoso. Per app B2B sul campo, mobility, sanita, e differenziatore competitivo significativo.

Pattern Repository e fondamentale per architettura offline-first: il layer Repository media tra UI e fonti di dati (DB locale, network), gestendo cache, sync, conflict. UI interagisce solo con Repository, mai direttamente con DB o API. Permette di testare logica indipendentemente da rete, scalare facilmente, migrare implementazioni.

Hai bisogno di aiuto?

Se vuoi sviluppare la tua app con G Tech Group, scrivici tramite il modulo di contatto.

Hai trovato utile quest'articolo?