(Facultatif) Créer un prototype et tester avec la suite d'émulateurs Firebase
Avant de voir comment votre application lit et écrit des données dans Realtime Database, présentons un ensemble d'outils que vous pouvez utiliser pour créer un prototype et tester les fonctionnalités de Realtime Database : la suite d'émulateurs Firebase. Si vous essayez différents modèles de données, optimisez vos règles de sécurité ou recherchez le moyen le plus rentable d'interagir avec le backend, il peut être judicieux de travailler en local sans déployer de services en direct.
Un émulateur Realtime Database fait partie de la suite d'émulateurs, qui permet à votre application d'interagir avec le contenu et la configuration de votre base de données émulée, ainsi qu'avec les ressources de votre projet émulé (fonctions, autres bases de données et règles de sécurité).emulator_suite_short
L'utilisation de l'émulateur Realtime Database ne nécessite que quelques étapes :
- Ajouter une ligne de code à la configuration de test de votre application pour vous connecter à l'émulateur.
- À partir de la racine du répertoire de votre projet local, exécutez
firebase emulators:start. - Effectuez des appels à partir du code de prototype de votre application à l'aide d'un SDK de plate-forme Realtime Database comme d'habitude, ou à l'aide de l'API REST Realtime Database.
Une procédure détaillée impliquant Realtime Database et Cloud Functions est disponible. Vous devriez également consulter l'introduction à la suite d'émulateurs .
Obtenir une DatabaseReference
Pour lire ou écrire des données dans la base de données, vous avez besoin d'une instance de DatabaseReference :
DatabaseReference ref = FirebaseDatabase.instance.ref();
Écrire des données
Ce document couvre les bases de la lecture et de l'écriture de données Firebase.
Les données Firebase sont écrites dans une DatabaseReference et récupérées en attendant ou en écoutant les événements émis par la référence. Les événements sont émis une fois pour l'état initial des données, puis chaque fois que les données changent.
Opérations d'écriture de base
Pour les opérations d'écriture de base, vous pouvez utiliser set() pour enregistrer des données dans une référence spécifiée, en remplaçant toutes les données existantes à ce chemin d'accès. Vous pouvez définir une référence sur les types suivants : String, boolean, int, double, Map, List.
Par exemple, vous pouvez ajouter un utilisateur avec set() comme suit :
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
L'utilisation de set() de cette manière écrase les données à l'emplacement spécifié, y compris tous les nœuds enfants. Toutefois, vous pouvez toujours mettre à jour un enfant sans réécrire l'objet entier. Si vous souhaitez autoriser les utilisateurs à mettre à jour leur profil, vous pouvez modifier le nom d'utilisateur comme suit :
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
La méthode update() accepte un sous-chemin d'accès aux nœuds, ce qui vous permet de mettre à jour plusieurs nœuds de la base de données à la fois :
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
Lire des données
Lire des données en écoutant les événements de valeur
Pour lire des données à un chemin d'accès et écouter les modifications, utilisez la propriété onValue de DatabaseReference pour écouter les DatabaseEvent.
Vous pouvez utiliser DatabaseEvent pour lire les données à un chemin d'accès donné, telles qu'elles existent au moment de l'événement. Cet événement est déclenché une fois lorsque l'écouteur est associé, puis chaque fois que les données, y compris les enfants, changent. L'événement comporte une propriété snapshot contenant toutes les données à cet emplacement, y compris les données enfants. Si aucune donnée n'est présente, la propriété exists de l'instantané est false et sa propriété value est nulle.
L'exemple suivant illustre une application de blog social qui récupère les détails d'un post à partir de la base de données :
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
L'écouteur reçoit un DataSnapshot qui contient les données à l'emplacement spécifié dans la base de données au moment de l'événement dans sa propriété value.
Lire des données une seule fois
Lire une seule fois à l'aide de get()
Le SDK est conçu pour gérer les interactions avec les serveurs de base de données, que votre application soit en ligne ou hors connexion.
En règle générale, vous devez utiliser les techniques d'événements de valeur décrites ci-dessus pour lire les données et être informé des mises à jour des données provenant du backend. Ces techniques réduisent votre utilisation et votre facturation, et sont optimisées pour offrir à vos utilisateurs la meilleure expérience possible lorsqu'ils sont en ligne et hors connexion.
Si vous n'avez besoin des données qu'une seule fois, vous pouvez utiliser get() pour obtenir un instantané des données de la base de données. Si, pour une raison quelconque, get() ne parvient pas à renvoyer la valeur du serveur, le client interroge le cache de stockage local et renvoie une erreur si la valeur n'est toujours pas trouvée.
L'exemple suivant illustre la récupération unique du nom d'utilisateur public d'un utilisateur à partir de la base de données :
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
L'utilisation inutile de get() peut augmenter l'utilisation de la bande passante et entraîner une perte de performances, ce qui peut être évité en utilisant un écouteur en temps réel comme indiqué ci-dessus.
Lire des données une seule fois avec once()
Dans certains cas, vous pouvez souhaiter que la valeur du cache local soit renvoyée immédiatement, au lieu de vérifier si une valeur mise à jour est disponible sur le serveur. Dans ce cas, vous pouvez utiliser once() pour obtenir immédiatement les données à partir du cache de disque local.
Cela est utile pour les données qui ne doivent être chargées qu'une seule fois et qui ne sont pas censées changer fréquemment ni nécessiter une écoute active. Par exemple, l'application de blog des exemples précédents utilise cette méthode pour charger le profil d'un utilisateur lorsqu'il commence à rédiger un post :
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
Mettre à jour ou supprimer des données
Mettre à jour des champs spécifiques
Pour écrire simultanément dans des enfants spécifiques d'un nœud sans écraser d'autres nœuds enfants, utilisez la méthode update().
Lorsque vous appelez update(), vous pouvez mettre à jour les valeurs enfants de niveau inférieur en spécifiant un chemin d'accès pour la clé. Si les données sont stockées à plusieurs emplacements pour une meilleure mise à l'échelle, vous pouvez mettre à jour toutes les instances de ces données à l'aide de la
distribution de données. Par exemple, une application de blog social peut vouloir créer un post et le mettre à jour simultanément dans le flux d'activité récent et dans le flux d'activité de l'utilisateur qui le publie. Pour ce faire, l'application de blog utilise un code semblable à celui-ci :
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
Cet exemple utilise push() pour créer un post dans le nœud contenant les posts de tous les utilisateurs à l'adresse /posts/$postid et récupérer simultanément la clé avec key. La clé peut ensuite être utilisée pour créer une deuxième entrée dans les posts de l'utilisateur à l'adresse /user-posts/$userid/$postid.
À l'aide de ces chemins d'accès, vous pouvez effectuer des mises à jour simultanées à plusieurs emplacements de l'arborescence JSON en un seul appel à update(), comme dans cet exemple qui crée le nouveau post aux deux emplacements. Les mises à jour simultanées effectuées de cette manière sont atomiques : toutes les mises à jour réussissent ou toutes échouent.
Ajouter un rappel de fin
Si vous souhaitez savoir quand vos données ont été validées, vous pouvez enregistrer des rappels de fin. set() et update() renvoient des Future, auxquels vous pouvez associer des rappels de réussite et d'erreur qui sont appelés lorsque l'écriture a été validée dans la base de données et lorsque l'appel a échoué.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Supprimer des données
Le moyen le plus simple de supprimer des données consiste à appeler remove() sur une référence à l'emplacement de ces données.
Vous pouvez également supprimer des données en spécifiant la valeur "null" pour une autre opération d'écriture telle que set() ou update(). Vous pouvez utiliser cette technique avec update() pour supprimer plusieurs enfants en un seul appel d'API.
Enregistrer des données en tant que transactions
Lorsque vous travaillez avec des données qui pourraient être corrompues par des modifications simultanées,
telles que des compteurs incrémentaux, vous pouvez utiliser une transaction en transmettant un
gestionnaire de transactions à runTransaction(). Un gestionnaire de transactions prend l'état actuel des données comme argument et renvoie le nouvel état souhaité que vous souhaitez écrire. Si un autre client écrit à l'emplacement avant que votre nouvelle valeur ne soit écrite, votre fonction de mise à jour est appelée à nouveau avec la nouvelle valeur actuelle, et l'écriture est retentée.
Par exemple, dans l'application de blog social, vous pouvez autoriser les utilisateurs à ajouter et à supprimer des posts de leurs favoris, et à suivre le nombre de favoris reçus par un post comme suit :
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
Par défaut, les événements sont déclenchés chaque fois que la fonction de mise à jour de la transaction s'exécute. Par conséquent, si vous exécutez la fonction plusieurs fois, vous pouvez voir des états intermédiaires.
Vous pouvez définir applyLocally sur false pour supprimer ces états intermédiaires et attendre que la transaction soit terminée avant que les événements ne soient déclenchés :
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
Le résultat d'une transaction est un TransactionResult, qui contient des informations indiquant si la transaction a été validée et le nouvel instantané :
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
Annuler une transaction
Si vous souhaitez annuler une transaction en toute sécurité, appelez Transaction.abort() pour
générer une AbortTransactionException :
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
Incréments atomiques côté serveur
Dans le cas d'utilisation ci-dessus, nous écrivons deux valeurs dans la base de données : l'ID de l'utilisateur qui ajoute ou supprime le post de ses favoris, et le nombre de favoris incrémenté. Si nous savons déjà que l'utilisateur ajoute le post à ses favoris, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
Ce code n'utilise pas d'opération de transaction. Il n'est donc pas réexécuté automatiquement en cas de mise à jour conflictuelle. Toutefois, comme l'opération d'incrémentation s'effectue directement sur le serveur de base de données, il n'y a aucun risque de conflit.
Si vous souhaitez détecter et rejeter des conflits spécifiques à l'application, par exemple lorsqu'un utilisateur ajoute un post à ses favoris alors qu'il l'a déjà fait, vous devez écrire des règles de sécurité personnalisées pour ce cas d'utilisation.
Utiliser des données hors connexion
Si un client perd sa connexion réseau, votre application continue de fonctionner correctement.
Chaque client connecté à une base de données Firebase conserve sa propre version interne des données actives. Lorsque des données sont écrites, elles sont d'abord écrites dans cette version locale. Le client Firebase synchronise ensuite ces données avec les serveurs de base de données à distance et avec d'autres clients sur la base du "meilleur effort".
Par conséquent, toutes les écritures dans la base de données déclenchent immédiatement des événements locaux, avant que les données ne soient écrites sur le serveur. Cela signifie que votre application reste réactive, quelle que soit la latence ou la connectivité du réseau.
Une fois la connectivité rétablie, votre application reçoit l'ensemble d'événements approprié afin que le client se synchronise avec l'état actuel du serveur, sans avoir à écrire de code personnalisé.
Pour en savoir plus sur le comportement hors connexion, consultez En savoir plus sur les fonctionnalités en ligne et hors connexion.