Activer les fonctionnalités hors connexion

Les applications Firebase fonctionnent même si votre application perd temporairement sa connexion réseau. De plus, Firebase fournit des outils pour rendre les données persistantes localement, gérer la présence et gérer la latence.

Persistance sur disque

Les applications Firebase gèrent automatiquement les interruptions réseau temporaires. Les données mises en cache sont disponibles hors connexion, et Firebase renvoie toutes les écritures lorsque la connectivité réseau est rétablie.

Lorsque vous activez la persistance sur disque, votre application écrit les données localement sur l'appareil. Elle peut ainsi conserver son état hors connexion, même si l'utilisateur ou le système d'exploitation redémarre l'application.

Vous pouvez activer la persistance sur disque avec une seule ligne de code.

FirebaseDatabase.instance.setPersistenceEnabled(true);

Comportement de la persistance

En activant la persistance, toutes les données que le client Firebase Realtime Database synchronise en ligne sont conservées sur le disque et sont disponibles hors connexion, même lorsque l'utilisateur ou le système d'exploitation redémarre l'application. Cela signifie que votre application fonctionne comme elle le ferait en ligne en utilisant les données locales stockées dans le cache. Les rappels d'écouteur continuent de se déclencher pour les mises à jour locales.

Le client Firebase Realtime Database conserve automatiquement une file d'attente de toutes les opérations d'écriture effectuées lorsque votre application est hors connexion. Lorsque la persistance est activée, cette file d'attente est également conservée sur le disque. Toutes vos écritures sont donc disponibles lorsque l'utilisateur ou le système d'exploitation redémarre l'application. Lorsque l'application retrouve la connectivité, toutes les opérations sont envoyées au serveur Firebase Realtime Database.

Si votre application utilise Firebase Authentication, le client Firebase Realtime Database conserve le jeton d'authentification de l'utilisateur lors des redémarrages de l'application. Si le jeton d'authentification expire lorsque votre application est hors connexion, le client met en pause les opérations d'écriture jusqu'à ce que votre application authentifie à nouveau l'utilisateur. Sinon, les opérations d'écriture peuvent échouer en raison des règles de sécurité.

Actualiser les données

Firebase Realtime Database synchronise et stocke une copie locale des données pour les écouteurs actifs. De plus, vous pouvez synchroniser des emplacements spécifiques.

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.keepSynced(true);

Le client Firebase Realtime Database télécharge automatiquement les données à ces emplacements et les synchronise même si la référence ne comporte aucun écouteur actif. Vous pouvez désactiver la synchronisation avec la ligne de code suivante.

scoresRef.keepSynced(false);

Par défaut, 10 Mo de données précédemment synchronisées sont mises en cache. Cela devrait suffire pour la plupart des applications. Si le cache dépasse la taille configurée, Firebase Realtime Database supprime les données les moins utilisées. Les données synchronisées ne sont pas supprimées du cache.

Interroger les données hors connexion

Firebase Realtime Database stocke les données renvoyées par une requête pour une utilisation hors connexion. Pour les requêtes créées hors connexion, Firebase Realtime Database continue de fonctionner pour les données précédemment chargées. Si les données demandées n'ont pas été chargées, Firebase Realtime Database charge les données à partir du cache local. Lorsque la connectivité réseau est à nouveau disponible, les données se chargent et reflètent la requête.

Par exemple, ce code interroge les quatre derniers éléments d'une base de données de scores :

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.orderByValue().limitToLast(4).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

Supposons que l'utilisateur perde la connexion, passe hors connexion et redémarre l'application. Toujours hors connexion, l'application interroge les deux derniers éléments du même emplacement. Cette requête renvoie les deux derniers éléments, car l'application avait chargé les quatre éléments de la requête ci-dessus.

scoresRef.orderByValue().limitToLast(2).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

Dans l'exemple précédent, le client Firebase Realtime Database déclenche des événements "enfant ajouté" pour les deux dinosaures ayant obtenu le meilleur score, à l'aide du cache persistant. Toutefois, il ne déclenche pas d'événement "valeur", car l'application n'a jamais exécuté cette requête en ligne.

Si l'application devait demander les six derniers éléments hors connexion, elle recevrait immédiatement des événements "enfant ajouté" pour les quatre éléments mis en cache. Lorsque l'appareil se reconnecte, le client Firebase Realtime Database se synchronise avec le serveur et reçoit les deux derniers événements "enfant ajouté" et l'événement "valeur" pour l'application.

Gérer les transactions hors connexion

Toutes les transactions effectuées lorsque l'application est hors connexion sont mises en file d'attente. Une fois que l'application retrouve la connectivité réseau, les transactions sont envoyées au serveur Realtime Database.

Firebase Realtime Database propose de nombreuses fonctionnalités pour gérer les scénarios hors connexion et la connectivité réseau. Le reste de ce guide s'applique à votre application, que la persistance soit activée ou non.

Gérer la présence

Dans les applications en temps réel, il est souvent utile de détecter quand les clients se connectent et se déconnectent. Par exemple, vous pouvez marquer un utilisateur comme "hors connexion" lorsque son client se déconnecte.

Les clients Firebase Database fournissent des primitives simples que vous pouvez utiliser pour écrire dans la base de données lorsqu'un client se déconnecte des serveurs Firebase Database. Ces mises à jour se produisent que le client se déconnecte correctement ou non. Vous pouvez donc compter sur elles pour nettoyer les données, même si une connexion est interrompue ou qu'un client plante. Toutes les opérations d'écriture, y compris la définition, la mise à jour et la suppression, peuvent être effectuées lors d'une déconnexion.

Voici un exemple simple d'écriture de données lors d'une déconnexion à l'aide de la primitive onDisconnect :

final presenceRef = FirebaseDatabase.instance.ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");

Fonctionnement de onDisconnect

Lorsque vous établissez une opération onDisconnect(), l'opération réside sur le serveur Firebase Realtime Database. Le serveur vérifie la sécurité pour s'assurer que l'utilisateur peut effectuer l'événement d'écriture demandé et informe votre application si elle n'est pas valide. Le serveur surveille ensuite la connexion. Si la connexion expire à un moment donné ou est fermée activement par le client Realtime Database, le serveur vérifie la sécurité une deuxième fois (pour s'assurer que l'opération est toujours valide), puis appelle l'événement.

try {
    await presenceRef.onDisconnect().remove();
} catch (error) {
    debugPrint("Could not establish onDisconnect event: $error");
}

Un événement onDisconnect peut également être annulé en appelant .cancel() :

final onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

Détecter l'état de la connexion

Pour de nombreuses fonctionnalités liées à la présence, il est utile que votre application sache quand elle est en ligne ou hors connexion. Firebase Realtime Database fournit un emplacement spécial à /.info/connected, qui est mis à jour chaque fois que l'état de la connexion du client Firebase Realtime Database change. Voici un exemple :

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    debugPrint("Connected.");
  } else {
    debugPrint("Not connected.");
  }
});

/.info/connected est une valeur booléenne qui n'est pas synchronisée entre les clients Realtime Database, car la valeur dépend de l'état du client. En d'autres termes, si un client lit /.info/connected comme "false", cela ne garantit pas qu'un autre client lira également "false".

Gérer la latence

Horodatages du serveur

Les serveurs Firebase Realtime Database fournissent un mécanisme permettant d'insérer des horodatages générés sur le serveur en tant que données. Combinée à onDisconnect, cette fonctionnalité permet de noter facilement et de manière fiable l'heure à laquelle un client Realtime Database s'est déconnecté :

final userLastOnlineRef =
    FirebaseDatabase.instance.ref("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(ServerValue.timestamp);

Décalage de l'horloge

Bien que ServerValue.timestamp soit beaucoup plus précis et préférable pour la plupart des opérations de lecture/écriture, il peut parfois être utile d'estimer le décalage de l'horloge du client par rapport aux serveurs de Firebase Realtime Database. Vous pouvez associer un rappel à l'emplacement /.info/serverTimeOffset pour obtenir la valeur, en millisecondes, que les clients Firebase Realtime Database ajoutent à l'heure locale signalée (heure de l'époque en millisecondes) afin d'estimer l'heure du serveur. Notez que la précision de ce décalage peut être affectée par la latence du réseau. Il est donc principalement utile pour détecter les écarts importants (> 1 seconde) dans l'heure de l'horloge.

final offsetRef = FirebaseDatabase.instance.ref(".info/serverTimeOffset");
offsetRef.onValue.listen((event) {
  final offset = event.snapshot.value as num? ?? 0.0;
  final estimatedServerTimeMs =
      DateTime.now().millisecondsSinceEpoch + offset;
});

Exemple d'application de présence

En combinant les opérations de déconnexion avec la surveillance de l'état de la connexion et les horodatages du serveur, vous pouvez créer un système de présence utilisateur. Dans ce système, chaque utilisateur stocke des données à un emplacement de base de données pour indiquer si un client Realtime Database est en ligne ou non. Les clients définissent cet emplacement sur "true" lorsqu'ils se connectent et sur un horodatage lorsqu'ils se déconnectent. Cet horodatage indique la dernière fois que l'utilisateur donné était en ligne.

Notez que votre application doit mettre en file d'attente les opérations de déconnexion avant qu'un utilisateur ne soit marqué comme étant en ligne, afin d'éviter toute condition de concurrence si la connexion réseau du client est perdue avant que les deux commandes puissent être envoyées au serveur.

// Since I can connect from multiple devices, we store each connection
// instance separately any time that connectionsRef's value is null (i.e.
// has no children) I am offline.
final myConnectionsRef =
    FirebaseDatabase.instance.ref("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final lastOnlineRef =
    FirebaseDatabase.instance.ref("/users/joe/lastOnline");

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    final con = myConnectionsRef.push();

    // When this device disconnects, remove it.
    con.onDisconnect().remove();

    // When I disconnect, update the last time I was seen online.
    lastOnlineRef.onDisconnect().set(ServerValue.timestamp);

    // Add this device to my connections list.
    // This value could contain info about the device or a timestamp too.
    con.set(true);
  }
});