Sync offline-first en Flutter + Firebase
Le cache local, source de vérité de l'UI
get() ou un snapshots() renvoie la donnée en cache tout de suite, puis une mise à jour arrive si le serveur diffère. L'UI n'attend jamais le réseau pour afficher quelque chose.
PersistentCacheSettings remplace les anciens champs persistenceEnabled et cacheSizeBytes , toujours acceptés mais dépréciés. Le cache a une taille plafonnée par défaut : Firestore évince les documents les moins récemment utilisés quand la limite est atteinte, et CACHE_SIZE_UNLIMITED la lève. Une lecture ponctuelle peut aussi forcer sa provenance avec GetOptions(source: Source.cache) ou Source.server , la valeur par défaut serverAndCache retombant sur le cache si le serveur ne répond pas.
Les écritures partent dans une file
set , update , delete et les WriteBatch rejoignent une file d'attente locale, appliquée au cache immédiatement et rejouée vers le serveur au retour du réseau. Les listeners attachés au document se déclenchent aussitôt avec la nouvelle valeur : c'est ce qui donne une UI optimiste sans code de rollback à écrire.
waitForPendingWrites() rend un Future qui se complète une fois la file vidée, pratique pour un indicateur « tout est synchronisé » ou pour séquencer un test.
Lire les métadonnées du snapshot
SnapshotMetadata avec deux drapeaux. isFromCache indique que la donnée vient du cache local et non d'une réponse serveur. hasPendingWrites indique qu'une écriture locale attend encore sa confirmation. Ensemble ils décrivent l'état de synchro exact d'un document.
includeMetadataChanges: true , le passage de hasPendingWrites à false (l'écriture confirmée par le serveur) ne déclenche aucun événement, et le badge « en cours de synchro » ne s'efface jamais.
La connectivité renseigne l'affichage
Conflits : last-write-wins par défaut
FieldValue.increment() contourne le problème : l'opération est commutative, donc des incréments rejoués depuis plusieurs appareils s'additionnent au lieu de s'écraser. Pour départager des mises à jour concurrentes, un updatedAt en FieldValue.serverTimestamp() donne un ordre déterministe, l'horodatage étant posé par le serveur au moment de la synchro, pas par l'horloge du téléphone.
serverTimestamp() se lit comme null dans le cache local, à gérer à l'affichage.
runTransaction est l'outil adapté, mais il ne fonctionne pas hors-ligne : une transaction exige un aller-retour serveur, elle n'est pas mise en file et échoue si le réseau manque. C'est voulu, puisqu'une transaction ne peut pas garantir son invariant sur un cache peut-être périmé. La logique qui ne tolère pas le last-write-wins vit donc côté serveur, dans la transaction ou une Cloud Function, et attend la connexion.
Tester le hors-ligne
disableNetwork() force le mode déconnecté, les écritures s'empilent dans la file, enableNetwork() la rejoue.
Avec Firestore, l'infrastructure hors-ligne est déjà en place : cache persistant, file d'écritures, synchro automatique au retour du réseau. Le travail d'une app offline-first tient surtout à faire confiance au cache local pour lire comme pour écrire, et à ne pas remettre le réseau au centre par réflexe.