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
- Definisci quali entita servono offline e quali solo online.
- Scegli uno storage locale solido (Room, GRDB, Realm).
- Imposta un layer di repository tra UI e dati.
- Marca ogni record con stato (synced, pending, conflict).
- Implementa una coda di operazioni con retry.
- Definisci la strategia di conflict resolution.
- 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.