Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider

Un cours complet pour maîtriser Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider.

0 chapitres 0 QCM Accès gratuit
 

Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider

  1. Objectif

    • Savoir utiliser les différents services de Firebase
  2. Tâche à réaliser

    • Tout d’abord, laissez-moi vous montrer la base de données.
    • Récupérer et Ajouter des Données dans Firestore avec Flutter et StreamProvider

    • Et les données seront affichées comme ceci :
  3. 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
    • Dans le fichier pubspec.yaml, ajoutez :
        dependencies:
          flutter:
            sdk: flutter
          firebase_core: ^2.0.0
          cloud_firestore: ^4.0.0
          provider: ^6.0.0
    • Puis exécutez :flutter pub get
  4. Afficher les données de Firebase

    1. 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.
          // 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();
          }
          
      • 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.
    2. Créez un fichier firebase_services.dart:
          // 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()
              );
            }
          }
        • 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.
    3. Créez la classe view_users_page
          // 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
                          );
                        },
                      );
                    },
                  ),
                ),
              );
            }
          }
          
  5. Ajouter un utilisateur dans Firestore dans Flutter

    1. Étape 1 : Créer la boîte de dialogue AddUserDialog
      • Créer un nouveau fichier: dans : lib/views/add_user_dialog.dart
          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'),
                  ),
                ],
              );
            }
          }
          
    2. Étape 2 : Ajouter la méthode addUser() dans FirebaseServices
           // Ajouter un utilisateur
            Future<void> addUser(UserModel user) async {
              await _db.collection('user').add({
                'name': user.name,
                'age': user.age,
              });
            }
    3. Étape 3 : Ajouter un FloatingActionButton
      • Modifie le Scaffold pour y inclure un floatingActionButton:
          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),
          ),

  6. 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.
    1. É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.
      • 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()),
            );
          }
        }
        
    2. Étape 2 : Ajouter les boutons Modifier & Supprimer dans la liste
      • Dans ViewUsersPage, modifie le ListTile:
      • 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!);
                                  }
                                },
                              ),
                            ],
                          ),
                        );
    3. Étape 3 : Adapter getUsers() pour passer l’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(),
            );
          }
      • 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
    4. Étape 4 : Ajouter la méthode deleteUser()
        // 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');
          }
        }
    5. Étape 5 : Créer EditUserDialog
        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'),
                )
              ],
            );
          }
        }
        
    6. Étape 6 : Ajouter addUser() dans FirebaseServices
        // 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,
            });
          }
    7. Étape 7 : Ajouter updateUser() dans FirebaseServices
        // 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,
            });
          }
    8. É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é.
      •   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)),
                                      ),
                                    ],
                                  ),
                                );
                              },
                            ),
                          ],
                        ),
                      );