Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider
Sommaire
- 1- Objectif
- 2- Tâche à réaliser
- 3- Créer un projet Firebase
- 4- Ajouter Firebase à votre projet Flutter
- 5- Afficher les données de Firebase
- 5.1- Créez la classe
UserModel
- 5.2- Créez un fichier
firebase_services.dart
: - 5.3- Créez la classe
view_users_page
- 6- Ajouter un utilisateur dans Firestore dans Flutter
- 6.1- Étape 1 : Créer la boîte de dialogue
AddUserDialog
- 6.2- Étape 2 : Ajouter la méthode
addUser()
dans FirebaseServices - 6.3- Étape 3 : Ajouter un
FloatingActionButton
- 7- Modifier et Supprimer un utilisateur depuis Firestore dans Flutter
- 7.1- Étape 1 : Modifier UserModel pour inclure l’ID du document Firestore
- 7.2- Étape 2 : Ajouter les boutons Modifier & Supprimer dans la liste
- 7.3- Étape 3 : Adapter getUsers() pour passer l’ID
- 7.4- Étape 4 : Ajouter la méthode
deleteUser()
- 7.5- Étape 5 : Créer EditUserDialog
- 7.6- Étape 6 : Ajouter addUser() dans FirebaseServices
- 7.7- Étape 7 : Ajouter updateUser() dans FirebaseServices
- 7.8- Étape 8 : Ajouter une alerte de confirmation avant suppression
- 7.8.1- Cours Flutter
Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider
-
Objectif
- Savoir utiliser les différents services de Firebase
-
Tâche à réaliser
- Tout d’abord, laissez-moi vous montrer la base de données.
- Et les données seront affichées comme ceci :
-
Créer un projet Firebase
- Rendez-vous sur https://console.firebase.google.com
- Créez un nouveau projet
- Activez Firestore dans l’onglet Build > Firestore Database
-
Ajouter Firebase à votre projet Flutter
- Dans le fichier
pubspec.yaml
, ajoutez : - Puis exécutez :
flutter pub get
-
Afficher les données de Firebase
-
Créez la classe
UserModel
- Il est temps de créer la classe du modèle => user_model.dart . Nous recevrons uniquement le nom et l’âge. Seuls deux champs sont donc nécessaires.
- Pourquoi créer une classe
UserModel
? - 1. Organisation du code (Modèle)
- C’est ce qu’on appelle un modèle de données. Ça permet de représenter clairement un utilisateur dans ton application.
- 2. Facilite l’échange de données
- Quand tu reçois des données depuis Firestore ou une API (généralement sous forme de Map<String, dynamic>), il est plus propre de les convertir en objets Dart (UserModel) au lieu de manipuler directement les maps.
- 3. Lisibilité et sécurité
- Travailler avec un UserModel rend ton code plus lisible, plus typé (tu sais que name est une String, age un int) et plus sûr que d’utiliser des maps avec des clés comme [‘name’] partout.
- 4. Utilisable avec Provider / StreamProvider
- Tu peux ensuite l’utiliser directement dans une StreamProvider<List<UserModel>> pour afficher une liste d’utilisateurs dans ton app.
-
Créez un fichier
firebase_services.dart
: - Partie 1 : Importations
- cloud_firestore : permet d’utiliser Firestore dans l’app Flutter.
- user_model.dart : on importe la classe UserModel qu’on a créée pour convertir les données Firestore en objets Dart.
- Partie 2 : Déclaration de la classe de service
FirebaseServices
: cette classe regroupe les opérations liées à Firestore (bonne pratique d’architecture)._db
: instance de Firebase Firestore utilisée pour interagir avec la base (lecture, ajout, suppression…).- Partie 3 : Méthode getUsers()
collection('user')
: Accède à la collection « user » dans Firestore..snapshots()
: Retourne un stream en temps réel de cette collection.- Toute modification (ajout/suppression/mise à jour) est immédiatement transmise à l’application Flutter.
.map(...)
: Transforme le snapshot Firestore en une liste d’objets UserModel.-
doc.data()
: Récupère les données de chaque document Firestore (sous forme de Map<String, dynamic>). UserModel.fromJson(...)
: Utilise le constructeur qu’on a défini pour transformer la Map en objet UserModel..toList()
: Convertit l’itération en une liste complète de UserModel.-
Créez la classe
view_users_page
-
Ajouter un utilisateur dans Firestore dans Flutter
-
Étape 1 : Créer la boîte de dialogue
AddUserDialog
- Créer un nouveau fichier: dans : lib/views/add_user_dialog.dart
-
Étape 2 : Ajouter la méthode
addUser()
dans FirebaseServices -
Étape 3 : Ajouter un
FloatingActionButton
- Modifie le Scaffold pour y inclure un
floatingActionButton
: -
Modifier et Supprimer un utilisateur depuis Firestore dans Flutter
- Dans cette étape il faut ajouter un id une nouvelle colonne, mais pourquoi ajouter le champ id ?
- Firestore stocke chaque document avec un identifiant unique (ID). Quand tu crées un utilisateur sans spécifier manuellement cet ID :
await FirebaseFirestore.instance.collection('user').add({...});
- Firestore génère automatiquement un ID aléatoire (comme A1bC2dEf3…).
- Mais si tu veux ensuite modifier ou supprimer un utilisateur spécifique, tu dois connaître cet ID, sinon tu ne pourras pas cibler le bon document.
- En ajoutant un champ id, tu peux :
- 1. Créer un document avec un ID que tu contrôles :
await FirebaseFirestore.instance.collection('user').doc(user.id).set(user.toJson());
- 2. Supprimer un document facilement :
await FirebaseFirestore.instance.collection('user').doc(user.id).delete();
- 3. Mettre à jour un utilisateur facilement :
await FirebaseFirestore.instance.collection('user').doc(user.id).update({
'name': newName,
'age': newAge,
}); - Dois-tu modifier le fichier JSON dans Firebase pour ajouter une nouvelle colonne (champ) ?
- Non, tu n’as pas besoin de modifier un fichier JSON dans Firebase.
- Firebase Firestore est une base de données NoSQL flexible, ce qui veut dire que tu peux ajouter un nouveau champ directement dans ton code Flutter, et Firestore va automatiquement l’enregistrer même s’il n’existait pas avant.
-
Étape 1 : Modifier UserModel pour inclure l’ID du document Firestore
- Firestore identifie les documents uniquement par leur ID
- Dans Firestore, chaque document possède un ID généré automatiquement (doc.id).
- Cet ID n’est pas inclus dans les données retournées par doc.data(), il est séparé.
- Pour pouvoir modifier (update) ou supprimer (delete) un document, il faut connaître son ID.
- Donc si tu veux faire :
FirebaseFirestore.instance.collection('users').doc(user.id).delete();
.
Tu dois obligatoirement avoir accès à l’ID quelque part. -
Étape 2 : Ajouter les boutons Modifier & Supprimer dans la liste
- Dans ViewUsersPage, modifie le ListTile :
-
Étape 3 : Adapter getUsers() pour passer l’ID
- Pourquoi
doc.id
est passé isolément dans UserModel.fromJson(…) et pas dans doc.data() ?
(doc) => UserModel.fromJson(doc.data(), doc.id)
Et non pas :(doc) => UserModel.fromJson(doc.data()) // Ça ne contient pas l’ID ! - doc.data() ne contient pas l’ID du document
- Firestore sépare l’identifiant (ID) du document de son contenu (data).
- L’ID est dans doc.id
- Donc si tu veux l’utiliser, tu dois le passer séparément.
- C’est pourquoi tu fais :
UserModel.fromJson(doc.data(), doc.id)
- Tu reçois :
- doc.data() → les champs name, age, etc.
doc.id → l'identifiant unique du document dans Firestore
-
Étape 4 : Ajouter la méthode
deleteUser()
-
Étape 5 : Créer EditUserDialog
-
Étape 6 : Ajouter addUser() dans FirebaseServices
-
Étape 7 : Ajouter updateUser() dans FirebaseServices
-
Étape 8 : Ajouter une alerte de confirmation avant suppression
- L’utilisateur doit voir une boîte de dialogue avec “Annuler” ou “Supprimer”.
- S’il confirme, on supprime via deleteUser.
- Tu peux aussi afficher un petit SnackBar en bas pour indiquer que l’utilisateur a été supprimé.
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.0.0
cloud_firestore: ^4.0.0
provider: ^6.0.0
// Classe représentant un utilisateur avec deux attributs : name et age
class UserModel {
// Attributs (ou propriétés) de l'utilisateur
String name;
int age;
// Constructeur principal : permet de créer un objet UserModel avec un nom et un âge
UserModel({required this.name, required this.age});
// Constructeur nommé 'fromJson' : permet de créer un objet UserModel à partir d'une Map (JSON)
// Utile pour convertir les données provenant de Firestore, d'une API REST, etc.
UserModel.fromJson(Map<String, dynamic> parsedJSON)
: name = parsedJSON['name'],
// Vérifie que 'age' est bien un int ; sinon, le convertit en int (ex : si c'est un double)
age = parsedJSON['age'] is int ? parsedJSON['age'] : parsedJSON['age'].toInt();
}
// Importation de la bibliothèque Firestore de Firebase
// Elle permet d’accéder aux collections et documents de la base de données Cloud Firestore
import 'package:cloud_firestore/cloud_firestore.dart';
// Importation du modèle de données UserModel (représente un utilisateur avec ses propriétés)
import '../models/user_model.dart';
// Déclaration de la classe de service Firebase
// Elle centralise toutes les interactions avec Firestore liées aux utilisateurs
class FirebaseServices {
// Création d'une instance unique de Firestore
// Cette instance (_db) permet d'effectuer toutes les opérations de lecture/écriture sur Firestore
final FirebaseFirestore _db = FirebaseFirestore.instance;
// Méthode pour récupérer les utilisateurs en temps réel depuis la collection 'user'
// Elle retourne un Stream (flux) de liste d'utilisateurs de type UserModel
Stream<List<UserModel>> getUsers() {
// Accès à la collection 'user' dans Firestore
// .snapshots() écoute les modifications en temps réel (ajout, suppression, modification)
return _db.collection('user').snapshots().map(
// Pour chaque snapshot (état actuel de la collection), on transforme chaque document...
(snapshot) => snapshot.docs.map(
// ...en objet UserModel à l'aide du constructeur fromJson
(doc) => UserModel.fromJson(doc.data()),
// Une fois que tous les documents sont convertis, on les transforme en liste
).toList()
);
}
}
// Importation des widgets Flutter nécessaires à l'interface utilisateur
import 'package:flutter/material.dart';
// Importation du package Provider pour la gestion d’état via StreamProvider et Consumer
import 'package:provider/provider.dart';
// Importation du modèle UserModel, qui représente la structure d’un utilisateur
import '../models/user_model.dart';
// Importation de la classe FirebaseServices, qui contient la méthode pour récupérer les utilisateurs
import '../services/firebase_services.dart';
// Définition d’un widget stateless pour afficher la liste des utilisateurs
class ViewUsersPage extends StatelessWidget {
// Création d’une instance de FirebaseServices pour accéder à Firestore
final FirebaseServices firebaseServices = FirebaseServices();
// Constructeur avec clé optionnelle
ViewUsersPage({super.key});
@override
Widget build(BuildContext context) {
// Utilisation de StreamProvider pour fournir un flux de List<UserModel> à l’arbre des widgets
return StreamProvider<List<UserModel>>(
// Source des données : appel à la méthode getUsers() qui retourne un Stream<List<UserModel>>
create: (_) => firebaseServices.getUsers(),
// Données initiales avant l’arrivée du flux réel
initialData: [],
// Scaffold : structure de base de la page
child: Scaffold(
// Barre d’application en haut de l’écran
appBar: AppBar(
elevation: 4,
title: Text('Liste des utilisateurs'), // Titre de l'appBar
centerTitle: true, // Centrage du titre
backgroundColor: Colors.amber, // Couleur de fond
),
// Body : zone principale du contenu
body: Consumer<List<UserModel>>(
// Consumer : écoute les données fournies par le StreamProvider (ici, List<UserModel>)
builder: (context, users, _) {
// Si la liste est vide, afficher un message
if (users.isEmpty) {
return Center(child: Text('Aucun utilisateur trouvé.'));
}
// Sinon, afficher une liste d’utilisateurs
return ListView.builder(
itemCount: users.length, // Nombre total d’éléments à afficher
itemBuilder: (_, index) {
final user = users[index]; // Récupérer un utilisateur à l’index donné
// Affichage d’un utilisateur avec ListTile
return ListTile(
leading: CircleAvatar(child: Icon(Icons.person)), // Avatar avec icône
title: Text(user.name), // Nom de l'utilisateur
subtitle: Text('${user.age} ans'), // Âge de l'utilisateur
);
},
);
},
),
),
);
}
}
import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/firebase_services.dart';
class AddUserDialog extends StatefulWidget {
const AddUserDialog({super.key});
@override
_AddUserDialogState createState() => _AddUserDialogState();
}
class _AddUserDialogState extends State<AddUserDialog> {
final _formKey = GlobalKey<FormState>();
String _name = '';
int _age = 0;
final FirebaseServices _firebaseServices = FirebaseServices();
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Ajouter un utilisateur'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Champ nom
TextFormField(
decoration: const InputDecoration(labelText: 'Nom'),
validator: (value) =>
value == null || value.isEmpty ? 'Entrez un nom' : null,
onSaved: (value) => _name = value!,
),
// Champ âge
TextFormField(
decoration: const InputDecoration(labelText: 'Âge'),
keyboardType: TextInputType.number,
validator: (value) =>
value == null || int.tryParse(value) == null
? 'Entrez un âge valide'
: null,
onSaved: (value) => _age = int.parse(value!),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
_firebaseServices.addUser(UserModel(name: _name, age: _age));
Navigator.pop(context);
}
},
child: const Text('Ajouter'),
),
],
);
}
}
// Ajouter un utilisateur
Future<void> addUser(UserModel user) async {
await _db.collection('user').add({
'name': user.name,
'age': user.age,
});
}
floatingActionButton: FloatingActionButton(
onPressed: () {
// Ouvrir une boîte de dialogue pour ajouter un utilisateur
showDialog(
context: context,
builder: (context) => AddUserDialog(),
);
},
backgroundColor: Colors.amber,
child: Icon(Icons.add),
),
class UserModel {
String? id; // maintenant String car Firestore génère une ID string
String name;
int age;
UserModel({this.id, required this.name, required this.age});
factory UserModel.fromJson(Map json, String id) {
return UserModel(
id: id,
name: json['name'],
age: json['age'] is int ? json['age'] : int.parse(json['age'].toString()),
);
}
}
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(user.name),
subtitle: Text('${user.age} ans'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () {
showDialog(
context: context,
builder: (_) => EditUserDialog(user: user),
);
},
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
if (user.id != null) {
firebaseServices.deleteUser(user.id!);
}
},
),
],
),
);
// Récupérer les utilisateurs en temps réel
Stream> getUsers() {
return _db.collection('user').snapshots().map(
(snapshot) => snapshot.docs.map(
(doc) => UserModel.fromJson(doc.data(), doc.id), // Utilisation de doc.id comme ID
).toList(),
);
}
// Méthode pour supprimer un utilisateur
// Méthode pour supprimer un utilisateur
Future deleteUser(String userId) async {
try {
await _db.collection('user').doc(userId).delete();
print('Utilisateur avec id=$userId supprimé');
} catch (e) {
print('Erreur lors de la suppression de l\'utilisateur : $e');
}
}
import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/firebase_services.dart';
class EditUserDialog extends StatefulWidget {
final UserModel user;
const EditUserDialog({super.key, required this.user});
@override
State<EditUserDialog> createState() => _EditUserDialogState();
}
class _EditUserDialogState extends State<EditUserDialog> {
final _formKey = GlobalKey<FormState>();
late String _name;
late int _age;
@override
void initState() {
super.initState();
_name = widget.user.name;
_age = widget.user.age;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Modifier utilisateur'),
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
initialValue: _name,
decoration: InputDecoration(labelText: 'Nom'),
validator: (value) =>
value == null || value.isEmpty ? 'Entrez un nom' : null,
onSaved: (value) => _name = value!,
),
TextFormField(
initialValue: _age.toString(),
decoration: InputDecoration(labelText: 'Âge'),
keyboardType: TextInputType.number,
validator: (value) =>
value == null || int.tryParse(value) == null
? 'Entrez un âge valide'
: null,
onSaved: (value) => _age = int.parse(value!),
),
],
),
),
actions: [
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
FirebaseServices().updateUser(
widget.user.id!, // doc ID
UserModel(name: _name, age: _age),
);
Navigator.of(context).pop();
}
},
child: Text('Modifier'),
)
],
);
}
}
// Ajouter un utilisateur
Future addUser(UserModel user) async {
await _db.collection('user').add({
'id': user.id, // Utilisation de l'ID de l'utilisateur
'name': user.name,
'age': user.age,
});
}
// Mettre à jour un utilisateur
Future updateUser(String userId, UserModel user) async {
await _db.collection('user').doc(userId).update({
'id': user.id,
'name': user.name,
'age': user.age,
});
}
final rootContext = context; // Conserve le contexte principal
............
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(user.name),
subtitle: Text('${user.age} ans'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () {
showDialog(
context: context,
builder: (_) => EditUserDialog(user: user),
);
},
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmation'),
content: Text('Voulez-vous vraiment supprimer "${user.name}" ?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await firebaseServices.deleteUser(user.id!);
// Utilisation du rootContext ici
ScaffoldMessenger.of(rootContext).showSnackBar(
SnackBar(
content: const Text('Utilisateur supprimé'),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
},
child: const Text('Supprimer', style: TextStyle(color: Colors.red)),
),
],
),
);
},
),
],
),
);