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

Travailler avec Cloud Firestore dans Flutter

Un cours complet pour maîtriser Travailler avec Cloud Firestore dans Flutter.

0 chapitres 0 QCM Accès gratuit
 
  1. Objectif

    • Comprendre ce qu'est Cloud Firestore
    • Découvrir les caractéristiques principales et la structure des données
    • Configurer Firebase dans un projet Flutter et effectuer les opérations CRUD
  2. Introduction à Cloud Firestore

    • Qu'est-ce que Cloud Firestore?
    • Cloud Firestore est une base de données NoSQL cloud, temps réel et scalable développée par Google.
    • Cloud Firestore est une base de données NoSQL flexible, évolutive et durable pour les applications mobiles, Web et serveur Google Cloud Platform. Il maintient vos données synchronisées sur tous les clients en temps réel et continue de fonctionner lorsque votre application est hors ligne, offrant ainsi une expérience utilisateur cohérente, réactive et fiable.
  3. Caractéristiques Principales

    • Fonctionnalité Description
    • Temps Réel Synchronisation automatique des données
    • Offline Fonctionne sans connexion internet
    • Scalable Gère automatiquement la charge
    • NoSQL Structure flexible en documents/collections
    • Sécurisé Règles de sécurité granulaires
  4. Architecture MVC en Flutter + Firestore exemple collection livre)

    • Objectif pédagogique: Comprendre comment circule l'information entre la Vue, le Contrôleur, le Modèle, et Firestore quand on ajoute un livre.
    • Schéma simplifié du flux
    • 
      Utilisateur ➜ View (Formulaire) ➜ Controller ➜ Model ➜ Firestore  
                ⬆︎                             ⬆︎          ⬆︎
            Affichage                Logique MÉTIER   Données stockées
    • Logique métier
      • La logique métier correspond à ce que fait ton application, et pourquoi elle le fait, par opposition à la logique technique (affichage, gestion de la base de données, etc.).
      • Exemple concret :
        • Imaginons une application de banque
        • Règle métier :
          • "Un compte ne peut pas être à découvert de plus de 1000 DT."
          • Dans MVC :
          • Model → contient la méthode retirer(argent) qui vérifie cette règle.
          • Controller → reçoit la requête de l’utilisateur ("je veux retirer 200 €"), appelle la méthode retirer() du modèle et choisit la vue à afficher selon le résultat.
          • View → affiche le message “Retrait réussi” ou “Découvert maximal atteint”.
        • En résumé :
          • La logique métier = les règles, calculs et processus propres au domaine fonctionnel de ton application.
          • Elle se trouve dans le Modèle, pas dans le Contrôleur ni dans la Vue.
        1. firebase_options.dart
            Fichier firebase_options.dart
            
            // File: lib/firebase_options.dart
            import 'package:firebase_core/firebase_core.dart';
            import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
            
            /*
            ? INSTRUCTIONS POUR LES ÉTUDIANTS:
            
            1. Récupérez vos clés Firebase sur https://console.firebase.google.com/
            2. Remplacez les valeurs ci-dessous par VOS propres clés
            3. NE COMMITTEZ PAS ce fichier avec vos vraies clés sur Git!
            4. Ajoutez-le dans .gitignore pour la sécurité
            
            ? Où trouver les valeurs:
            
            • apiKey → google-services.json → client → api_key → current_key
            • appId → google-services.json → client → client_info → mobilesdk_app_id  
            • messagingSenderId → google-services.json → project_info → project_number
            • projectId → google-services.json → project_info → project_id
            • storageBucket → google-services.json → project_info → storage_bucket
            */
            
            /// CONFIGURATION FIREBASE POUR TOUTES LES PLATEFORMES
            class DefaultFirebaseOptions {
              
              /// CONFIGURATION ANDROID
              static const FirebaseOptions android = FirebaseOptions(
                apiKey: '.....',
                appId: '.....',
                messagingSenderId: '.....',
                projectId: '......',
                storageBucket: '.......',
              );
            
              /// CONFIGURATION iOS (À décommenter si nécessaire)
              /*
              static const FirebaseOptions ios = FirebaseOptions(
                apiKey: 'AIzaSyA1B2C3d4E5f6G7H8I9J0K1L2M3N4O5P6Q',
                appId: '1:835574812736:ios:abc123def456ghi789',
                messagingSenderId: '835574812736',
                projectId: 'gestion-bibliotheque-7ed2f',
                storageBucket: 'gestion-bibliotheque-7ed2f.firebasestorage.app',
                iosBundleId: 'com.example.gestionBibliotheque',
              );
              */
            
              /// CONFIGURATION WEB (À décommenter si nécessaire)
              /*
              static const FirebaseOptions web = FirebaseOptions(
                apiKey: 'AIzaSyA1B2C3d4E5f6G7H8I9J0K1L2M3N4O5P6Q',
                appId: '1:835574812736:web:abc123def456ghi789',
                messagingSenderId: '835574812736',
                projectId: 'gestion-bibliotheque-7ed2f',
                authDomain: 'gestion-bibliotheque-7ed2f.firebaseapp.com',
                storageBucket: 'gestion-bibliotheque-7ed2f.firebasestorage.app',
              );
              */
            
              /// ? DÉTECTION AUTOMATIQUE DE LA PLATEFORME
              static FirebaseOptions get currentPlatform {
                // Si on est sur le web
                if (kIsWeb) {
                  // return web; // Décommenter quand web est configuré
                  throw UnsupportedError(
                    'Configuration Web non encore configurée. '
                    'Veuillez décommenter et configurer la section web ci-dessus.',
                  );
                }
                
                // Si on est sur mobile
                switch (defaultTargetPlatform) {
                  case TargetPlatform.android:
                    return android;
                  case TargetPlatform.iOS:
                    // return ios; // Décommenter quand iOS est configuré
                    throw UnsupportedError(
                      'Configuration iOS non encore configurée. '
                      'Veuillez décommenter et configurer la section iOS ci-dessus.',
                    );
                  default:
                    throw UnsupportedError(
                      'Plateforme non supportée: ${defaultTargetPlatform}. '
                      'Seul Android est configuré pour le moment.',
                    );
                }
              }
            }
        2. main.dart)
            Fichier main.dart
            import 'package:flutter/material.dart';
            import 'package:firebase_core/firebase_core.dart';
            import 'package:provider/provider.dart';
            import 'firebase_options.dart'; // ← Fichier de configuration
            import 'controllers/livre_controller.dart';
            //import 'views/liste_livres_view.dart';
            import 'views/liste_livres_view_recherche.dart';
            
            void main() async {
              WidgetsFlutterBinding.ensureInitialized();
              
              // Initialisation Firebase avec la configuration spécifique
              await Firebase.initializeApp(
                options: DefaultFirebaseOptions.currentPlatform, // ← Utilise notre config
              );
              
              runApp(MyApp());
            }
            
            class MyApp extends StatelessWidget {
              @override
              Widget build(BuildContext context) {
                return MultiProvider(
                  providers: [
                    Provider<LivreController>(
                      create: (_) => LivreController(),
                    ),
                  ],
                  child: MaterialApp(
                    title: 'Ma Bibliothèque',
                     // theme: ThemeData(
                    //   primarySwatch: Colors.blue,
                    //   useMaterial3: true,
                    // ),
                    theme: ThemeData(
                      useMaterial3: false, // Pour compatibilité avec cardTheme comme ci-dessous
                      primaryColor: const Color(0xFF4CAF50),
                      scaffoldBackgroundColor: const Color(0xFFF5F5F5),
                      appBarTheme: const AppBarTheme(
                        backgroundColor: Color(0xFF4CAF50),
                        foregroundColor: Colors.white,
                      ),
                      floatingActionButtonTheme: const FloatingActionButtonThemeData(
                        backgroundColor: Color(0xFF4CAF50),
                      ),
                      cardTheme: CardThemeData( // ← ICI : CardThemeData au lieu de CardTheme ancienne version
                        color: Colors.white,
                        elevation: 4,
                        shadowColor: Colors.black12,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.all(Radius.circular(12)),
                        ),
                      ),
                      inputDecorationTheme: InputDecorationTheme(
                        filled: true,
                        fillColor: Colors.white,
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                          borderSide: BorderSide.none,
                        ),
                      ),
                      popupMenuTheme: PopupMenuThemeData(
              color: Colors.white,
              elevation: 8,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(12),
                side: BorderSide(color: Colors.grey[300]!, width: 1),
              ),
              textStyle: TextStyle(
                color: Colors.grey[800],
                fontSize: 14,
                fontWeight: FontWeight.w500,
              ),
             
            ),
                    ),
                    home: const ListeLivresView(),
                    debugShowCheckedModeBanner: false,
                  ),
                );
              }
            }
        3. MODÈLE (models/livre_model.dart)
          • Rôle: Définir la structure des données.
          • Comprendre que le modèle est la véritable représentation du livre.
          • Fichier livre_model.dart
            class Livre {
              String? id;
              String titre;
              String auteur;
              int annee;
              
              Livre({
                this.id,
                required this.titre,
                required this.auteur,
                required this.annee,
              });
              
              // Convertir Livre en Map pour Firestore
              Map<String, dynamic> toMap() {
                return {
                  'titre': titre,
                  'auteur': auteur,
                  'annee': annee,
                };
              }
              
              // Créer Livre depuis Firestore document
              factory Livre.fromFirestore(Map<String, dynamic> data, String id) {
                return Livre(
                  id: id,
                  titre: data['titre'] ?? '',
                  auteur: data['auteur'] ?? '',
                  annee: data['annee'] ?? 0,
                );
              }
            }
          • À retenir :
            • On crée une classe Livre pour garder un code propre et clair.
            • .toMap() permet de transformer l’objet en format compréhensible par Firestore.
        4. CONTROLLER (controllers/livre_controller.dart)
          • Rôle: Gérer la logique métier.
          • C'est le "chef d'orchestre" entre la vue et la base de données.
          • Fichier livre_controller.dart
            import 'package:cloud_firestore/cloud_firestore.dart';
            import '../models/livre_model.dart';
            
            class LivreController {
              // Instance principale de Firestore (point d’accès à la base de données)
              final FirebaseFirestore _firestore = FirebaseFirestore.instance;
              
              // Référence à la collection "livres" dans Firestore
              // Cela évite de répéter _firestore.collection('livres') partout
              CollectionReference get _livresRef => _firestore.collection('livres');
              
              // ======================
              // ? CREATE - Ajouter un livre
              // ======================
              Future<void> ajouterLivre(Livre livre) async {
                try {
                  // Ajoute un nouveau document dans la collection "livres"
                  await _livresRef.add(livre.toMap());
                } catch (e) {
                  throw 'Erreur lors de l\'ajout: $e';
                }
              }
              
              // ======================
              // ? READ - Récupérer tous les livres (en temps réel)
              // ======================
              Stream<List<Livre>> getLivresStream() {
                // Écoute en continu la collection "livres" (avec snapshots)
                // Chaque changement (ajout, modif, suppression) est automatiquement émis dans le Stream
                return _livresRef.snapshots().map((snapshot) {
                  // Convertit chaque document Firestore en objet Livre
                  return snapshot.docs.map((doc) {
                    return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                  }).toList();
                });
              }
              
              // ======================
              // ? READ - Récupérer un livre par son ID
              // ======================
              Future<Livre?> getLivreById(String id) async {
                try {
                  // Récupère un document spécifique selon son ID
                  final doc = await _livresRef.doc(id).get();
                  
                  // Si le document existe, on le transforme en objet Livre
                  if (doc.exists) {
                    return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                  }
                  // Sinon, on retourne null
                  return null;
                } catch (e) {
                  throw 'Erreur lors de la récupération: $e';
                }
              }
              
              // ======================
              // ? UPDATE - Modifier un livre existant
              // ======================
              Future<void> modifierLivre(String id, Livre livre) async {
                try {
                  // Met à jour le document correspondant à l’ID avec les nouvelles données
                  await _livresRef.doc(id).update(livre.toMap());
                } catch (e) {
                  throw 'Erreur lors de la modification: $e';
                }
              }
              
              // ======================
              // ? DELETE - Supprimer un livre
              // ======================
              Future<void> supprimerLivre(String id) async {
                try {
                  // Supprime le document correspondant à l’ID fourni
                  await _livresRef.doc(id).delete();
                } catch (e) {
                  throw 'Erreur lors de la suppression: $e';
                }
              }
              
              // ======================
              // ? SEARCH - Rechercher des livres par titre
              // ======================
              Stream<List<Livre>> rechercherLivres(String query) {
                // Effectue une recherche "commence par" sur le champ 'titre'
                // grâce à isGreaterThanOrEqualTo et isLessThan
                return _livresRef
                    .where('titre', isGreaterThanOrEqualTo: query)
                    .where('titre', isLessThan: query + 'z')
                    .snapshots()
                    .map((snapshot) {
                      // Transforme les résultats Firestore en liste de Livres
                      return snapshot.docs.map((doc) {
                        return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                      }).toList();
                    });
              }
            }
          • À retenir :
            • Le contrôleur prépare la donnée avant l’envoi.
            • On y trouve souvent try/catch pour bien gérer les erreurs.
            • La vue ne parle pas directement à Firestore → elle passe par le contrôleur.
        5. VUE (views/ajouter_livre.dart)
          • Rôle : Afficher l’interface et récupérer les données de l’utilisateur.
          • Fichier: ajouter_livre.dart
            import 'package:flutter/material.dart';
            import 'package:provider/provider.dart';
            import '../controllers/livre_controller.dart';
            import '../models/livre_model.dart';
            
            class AjouterLivreView extends StatefulWidget {
              const AjouterLivreView({super.key});
            
              @override
              State<AjouterLivreView> createState() => _AjouterLivreViewState();
            }
            
            class _AjouterLivreViewState extends State<AjouterLivreView> {
              final _formKey = GlobalKey<FormState>();
              final _titreController = TextEditingController();
              final _auteurController = TextEditingController();
              final _anneeController = TextEditingController();
              
              bool _isLoading = false;
            
              @override
              Widget build(BuildContext context) {
                final livreController = Provider.of<LivreController>(context, listen: false);
            
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Ajouter un livre'),
                  ),
                  body: Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Form(
                      key: _formKey,
                      child: Column(
                        children: [
                          TextFormField(
                            controller: _titreController,
                            decoration: const InputDecoration(
                              labelText: 'Titre',
                              border: OutlineInputBorder(),
                            ),
                            validator: (value) {
                              if (value == null || value.isEmpty) {
                                return 'Veuillez entrer un titre';
                              }
                              return null;
                            },
                          ),
                          const SizedBox(height: 16),
                          TextFormField(
                            controller: _auteurController,
                            decoration: const InputDecoration(
                              labelText: 'Auteur',
                              border: OutlineInputBorder(),
                            ),
                            validator: (value) {
                              if (value == null || value.isEmpty) {
                                return 'Veuillez entrer un auteur';
                              }
                              return null;
                            },
                          ),
                          const SizedBox(height: 16),
                          TextFormField(
                            controller: _anneeController,
                            decoration: const InputDecoration(
                              labelText: 'Année',
                              border: OutlineInputBorder(),
                            ),
                            keyboardType: TextInputType.number,
                            validator: (value) {
                              if (value == null || value.isEmpty) {
                                return 'Veuillez entrer une année';
                              }
                              final annee = int.tryParse(value);
                              if (annee == null || annee < 0) {
                                return 'Veuillez entrer une année valide';
                              }
                              return null;
                            },
                          ),
                          const SizedBox(height: 24),
                          _isLoading
                              ? const CircularProgressIndicator()
                              : ElevatedButton(
                                  onPressed: _submitForm,
                                  child: const Text('Ajouter le livre'),
                                ),
                        ],
                      ),
                    ),
                  ),
                );
              }
              
              Future<void> _submitForm() async {
                if (_formKey.currentState!.validate()) {
                  setState(() => _isLoading = true);
                  
                  final livreController = Provider.of<LivreController>(context, listen: false);
                  final nouveauLivre = Livre(
                    titre: _titreController.text,
                    auteur: _auteurController.text,
                    annee: int.parse(_anneeController.text),
                  );
                  
                  try {
                    await livreController.ajouterLivre(nouveauLivre);
                    Navigator.pop(context);
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Livre ajouté avec succès')),
                    );
                  } catch (e) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Erreur: $e')),
                    );
                  } finally {
                    setState(() => _isLoading = false);
                  }
                }
              }
              
              @override
              void dispose() {
                _titreController.dispose();
                _auteurController.dispose();
                _anneeController.dispose();
                super.dispose();
              }
            }
          • À retenir :
            • La vue affiche uniquement l’interface → pas de logique Firestore ici !
            • Elle utilise des champs TextField pour capturer les données.
            • Au clic, elle crée un Livre puis appelle une méthode du controller.
            Fichier: liste_livre.dart
            import 'package:flutter/material.dart';
            import 'package:provider/provider.dart';
            import '../controllers/livre_controller.dart';
            import '../models/livre_model.dart';
            import 'ajouter_livre_view.dart';
            
            class ListeLivresView extends StatelessWidget {
              const ListeLivresView({super.key});
            
              @override
              Widget build(BuildContext context) {
                final livreController = Provider.of<LivreController>(context, listen: false);
            
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Ma Bibliothèque'),
                    actions: [
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => AjouterLivreView()),
                          );
                        },
                      ),
                    ],
                  ),
                  body: StreamBuilder<List<Livre>>(
                    stream: livreController.getLivresStream(),
                    builder: (context, snapshot) {
                      if (snapshot.hasError) {
                        return Center(child: Text('Erreur: ${snapshot.error}'));
                      }
                      
                      if (snapshot.connectionState == ConnectionState.waiting) {
                        return const Center(child: CircularProgressIndicator());
                      }
                      
                      final livres = snapshot.data ?? [];
                      
                      if (livres.isEmpty) {
                        return const Center(child: Text('Aucun livre trouvé'));
                      }
                      
                      return ListView.builder(
                        itemCount: livres.length,
                        itemBuilder: (context, index) {
                          final livre = livres[index];
                          return LivreTile(livre: livre);
                        },
                      );
                    },
                  ),
                  floatingActionButton: FloatingActionButton(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => AjouterLivreView()),
                      );
                    },
                    child: const Icon(Icons.add),
                  ),
                );
              }
            }
            
            class LivreTile extends StatelessWidget {
              final Livre livre;
              
              const LivreTile({super.key, required this.livre});
            
              @override
              Widget build(BuildContext context) {
                final livreController = Provider.of<LivreController>(context, listen: false);
            
                return Card(
                  margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  child: ListTile(
                    title: Text(livre.titre),
                    subtitle: Text('${livre.auteur} - ${livre.annee}'),
                    trailing: PopupMenuButton(
                      onSelected: (value) {
                        switch (value) {
                          case 'modifier':
                            // Navigation vers modification
                            break;
                          case 'supprimer':
                            _showDeleteDialog(context, livreController, livre.id!);
                            break;
                        }
                      },
                      itemBuilder: (context) => [
                        const PopupMenuItem(value: 'modifier', child: Text('Modifier')),
                        const PopupMenuItem(value: 'supprimer', child: Text('Supprimer')),
                      ],
                    ),
                  ),
                );
              }
              
              void _showDeleteDialog(BuildContext context, LivreController controller, String id) {
                showDialog(
                  context: context,
                  builder: (context) => AlertDialog(
                    title: const Text('Confirmer la suppression'),
                    content: const Text('Voulez-vous vraiment supprimer ce livre ?'),
                    actions: [
                      TextButton(
                        onPressed: () => Navigator.pop(context),
                        child: const Text('Annuler'),
                      ),
                      TextButton(
                        onPressed: () async {
                          try {
                            await controller.supprimerLivre(id);
                            Navigator.pop(context);
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Livre supprimé avec succès')),
                            );
                          } catch (e) {
                            Navigator.pop(context);
                            ScaffoldMessenger.of(context).showSnackBar(
                              SnackBar(content: Text('Erreur: $e')),
                            );
                          }
                        },
                        child: const Text('Supprimer'),
                      ),
                    ],
                  ),
                );
              }
            }
          • Ajouter la partie recherche
          • Fichier: liste_livres_view_recherche.dart
            import '../views/modifier_livre_view.dart';
            import 'package:flutter/material.dart';
            import 'package:provider/provider.dart';
            import '../controllers/livre_controller.dart';
            import '../models/livre_model.dart';
            import 'ajouter_livre_view.dart';
            
            class ListeLivresView extends StatefulWidget {
              const ListeLivresView({super.key});
            
              @override
              State<ListeLivresView> createState() => _ListeLivresViewState();
            }
            
            class _ListeLivresViewState extends State<ListeLivresView> {
              String _searchQuery = '';
            
              @override
              Widget build(BuildContext context) {
                final livreController = Provider.of<LivreController>(context, listen: false);
            
                // On choisit le flux en fonction du champ de recherche
                final Stream<List<Livre>> livresStream = _searchQuery.isEmpty
                    ? livreController.getLivresStream()
                    : livreController.rechercherLivres(_searchQuery);
            
                return Scaffold(
                  appBar: AppBar(
                    title: const Text('Ma Bibliothèque'),
                    actions: [
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () {
                          Navigator.push(
                            context,
                            MaterialPageRoute(builder: (context) => const AjouterLivreView()),
                          );
                        },
                      ),
                    ],
                  ),
                  body: Column(
                    children: [
                      // ? Zone de recherche
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: TextField(
                          decoration: InputDecoration(
                            prefixIcon: const Icon(Icons.search),
                            hintText: 'Rechercher un livre par titre...',
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                            ),
                          ),
                          onChanged: (value) {
                            setState(() {
                              _searchQuery = value.trim();
                            });
                          },
                        ),
                      ),
            
                      // ? Liste des livres
                      Expanded(
                        child: StreamBuilder<List<Livre>>(
                          stream: livresStream,
                          builder: (context, snapshot) {
                            if (snapshot.hasError) {
                              return Center(child: Text('Erreur: ${snapshot.error}'));
                            }
            
                            if (snapshot.connectionState == ConnectionState.waiting) {
                              return const Center(child: CircularProgressIndicator());
                            }
            
                            final livres = snapshot.data ?? [];
            
                            if (livres.isEmpty) {
                              return const Center(child: Text('Aucun livre trouvé'));
                            }
            
                            return ListView.builder(
                              itemCount: livres.length,
                              itemBuilder: (context, index) {
                                final livre = livres[index];
                                return LivreTile(livre: livre);
                              },
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                  floatingActionButton: FloatingActionButton(
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const AjouterLivreView()),
                      );
                    },
                    child: const Icon(Icons.add),
                  ),
                );
              }
            }
            
            class LivreTile extends StatelessWidget {
              final Livre livre;
            
              const LivreTile({super.key, required this.livre});
            
              @override
              Widget build(BuildContext context) {
                final livreController = Provider.of<LivreController>(context, listen: false);
            
                return Card(
                  margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  child: ListTile(
                    title: Text(livre.titre),
                    subtitle: Text('${livre.auteur} - ${livre.annee}'),
                    trailing: PopupMenuButton(
                      onSelected: (value) {
                        switch (value) {
                          case 'modifier':
                             Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => ModifierLivreView(livre: livre),
                      ),
                    );
                    break;
                  case 'supprimer':
                    _showDeleteDialog(context, livreController, livre.id!);
                    break;
                }
              },
                      itemBuilder: (context) => [
              PopupMenuItem(
                value: 'modifier',
                child: Row(
                  children: [
                    Icon(Icons.edit, color: Color(0xFF4CAF50), size: 20),
                    const SizedBox(width: 8),
                    const Text('Modifier'),
                  ],
                ),
              ),
              PopupMenuItem(
                value: 'supprimer',
                child: Row(
                  children: [
                    Icon(Icons.delete, color: Colors.red[600], size: 20),
                    const SizedBox(width: 8),
                    const Text('Supprimer'),
                  ],
                ),
              ),
            ],
                    ),
                  ),
                );
              }
            
              void _showDeleteDialog(
                  BuildContext context, LivreController controller, String id) {
                showDialog(
                  context: context,
                  builder: (context) => AlertDialog(
                    title: const Text('Confirmer la suppression'),
                    content: const Text('Voulez-vous vraiment supprimer ce livre ?'),
                    actions: [
                      TextButton(
                        onPressed: () => Navigator.pop(context),
                        child: const Text('Annuler'),
                      ),
                      TextButton(
                        onPressed: () async {
                          try {
                            await controller.supprimerLivre(id);
                            Navigator.pop(context);
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('Livre supprimé avec succès')),
                            );
                          } catch (e) {
                            Navigator.pop(context);
                            ScaffoldMessenger.of(context).showSnackBar(
                              SnackBar(content: Text('Erreur: $e')),
                            );
                          }
                        },
                        child: const Text('Supprimer'),
                      ),
                    ],
                  ),
                );
              }
            }
  5. Projet à réaliser

    • Créer une application mobile permettant de gérer une collection de livres avec :
        ✅ Ajouter de nouveaux livres ? Consulter la liste des livres ✏️ Modifier les informations d'un livre ?️ Supprimer des livres ? Rechercher des livres
      Architecture MVC imposée
        lib/
        ├── main.dart                      # Point d'entrée de l'application
        ├── firebase_options.dart          # Configuration Firebase
        ├── models/
        │   └── livre_model.dart           # Modèle de données
        ├── controllers/
        │   └── livre_controller.dart      # Logique métier & Firestore
        └── views/
            ├── liste_livres_view.dart     # Écran principal
            └── ajouter_livre_view.dart    # Formulaire d'ajout
  6. Configuration du projet Flutter avec Firestore

    1. Fichier main.dart
        
        void main() async {
          WidgetsFlutterBinding.ensureInitialized();
          
          // Initialisation Firebase avec la configuration spécifique
          await Firebase.initializeApp(
            options: DefaultFirebaseOptions.currentPlatform, // ← Utilise notre config
          );
          
          runApp(MyApp());
        }
        
    2. Modifier les règles Firestore
      • Allez dans la Console Firebase → Firestore Database → Règles
      • REMPLACEZ les règles actuelles par :
          rules_version = '2';
          service cloud.firestore {
            match /databases/{database}/documents {
              // RÈGLES POUR DÉVELOPPEMENT (À CHANGER EN PRODUCTION)
              match /livres/{document} {
                allow read, write: if true; // Autorise tout le monde
              }
              
              // OU pour tester avec authentification :
              // match /livres/{document} {
              //   allow read, write: if request.auth != null;
              // }
            }
          }
          
  7. Effectuer des opérations CRUD dans Firestore

    • Firestore vous permet d'effectuer des opérations de création, de lecture, de mise à jour et de suppression (CRUD) sur vos données. Voyons comment procéder dans Flutter.
      1. Règles et bonnes pratiques pour créer un document dans Firestore :
        1. Utiliser des modèles (classes) pour représenter les données

          • Toujours créer une classe Livre (ou autre) pour représenter un document :
          • class Livre {
              final String titre;
              final String auteur;
              final int annee;
            
              Livre({required this.titre, required this.auteur, required this.annee});
            
              Map toMap() {
                return {
                  'titre': titre,
                  'auteur': auteur,
                  'annee': annee,
                };
              }
            }
          • Avantage: cohérence, lisibilité du code, moins d'erreurs lors du stockage.
        2. Utiliser add() ou doc().set() selon le besoin

          • Dans Firestore, vous pouvez créer un document dans une collection de deux manières :
          • Avec .add() ➜ Id auto-généré par Firestore :
              await FirebaseFirestore.instance
                      .collection('livres')
                      .add(livre.toMap());
              
          • Avec .doc(id).set() ➜ Id personnalisé :
            • Vous choisissez vous-même l’ID du document.
            • Utile quand vous devez garantir l'unicité ou utiliser un ID déjà connu.
            • Donne un meilleur contrôle sur la structure de votre base de données.
            • await FirebaseFirestore.instance
                      .collection('livres')
                      .doc(livreId)
                      .set(livre.toMap());
              
        3. Toujours gérer les exceptions
          • Exemple :
          • try {
              await _livresRef.add(livre.toMap());
            } catch (e) {
              print("Erreur lors de l'ajout: $e");
            }
            Important : ne jamais laisser une erreur Firestore passer en silence.
  8. Lire un document dans Firestore:
    • Firestore propose 2 niveaux de lecture :
      • Document spécifique (via son ID)
      • Tous les documents d’une collection
    • Et 2 modes de récupération :
      • En continu (temps réel) avec snapshots() ➜ Retourne un Stream
      • Une seule fois (ponctuel) avec get() ➜ Retourne un Future
      1. Lire tous les documents d’une collection

        • En temps réel (Stream)
          • Mise à jour automatique des données dès qu’un changement a lieu.
          • 
            Stream, doc.id);
                }).toList();
              });
            }
          • Utilisation typique :
            • Afficher une liste de livres dans un StreamBuilder.
            • Utile pour un flux dynamique d'informations (ex : messages chat, liste mise à jour en direct).
      2. Lecture ponctuelle (Future)
        • Lecture "one shot", uniquement au moment où le code est exécuté.
        • Future<List<Livre>> getTousLesLivres() async {
            try {
              final snapshot = await _livresRef.get();
              return snapshot.docs
                  .map((doc) => Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id))
                  .toList();
            } catch (e) {
              throw 'Erreur lors de la récupération : $e';
            }
          }
        • Utilisation typique :
          • Charger une liste statique, non mise à jour automatiquement.
          • Exemple: Exporter une liste d’objets dans un fichier Excel.
      3. Lire un document spécifique via son ID

        • En temps réel (Stream)
          • Pour suivre les changements en direct sur un seul document.
          • Stream<Livre?> getLivreStreamById(String id) {
              return _livresRef.doc(id).snapshots().map((doc) {
                if (doc.exists) {
                  return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                }
                return null;
              });
            }
          • Utilisation typique :
          • Suivre l'état d’un document en particulier (ex : stock en temps réel, statut de commande).
        • Lecture ponctuelle (Future)
          • Lecture ciblée, destinée à une action ponctuelle ou une navigation.
          • C’est idéal pour récupérer les données d’un seul document lors d’une navigation ou d’une action spécifique, comme ouvrir une page de détails pour un livre.
          • Il utilise await ... .get() → cela indique une lecture une seule fois, au moment précis où tu appelles la fonction.
          • Future<Livre?> getLivreById(String id) async {
              try {
                final doc = await _livresRef.doc(id).get();
                if (doc.exists) {
                  return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                }
                return null;
              } catch (e) {
                throw 'Erreur lors de la récupération: $e';
              }
            }
          • Utilisation typique :
          • Récupérer les détails d'un élément lors d’une navigation vers une nouvelle page.
      4. Récapitulatif visuel

      5. Lecture Cible Mode Retourne Exemples
        .collection().snapshots() Tous les documents Temps réel Stream<List> Liste des livres qui se met à jour automatiquement
        .collection().get() Tous les documents Ponctuel Future<List> Exporter tous les livres une seule fois
        .doc(id).snapshots() Document spécifique Temps réel Stream Observer l'état d’un seul livre (ex : stock, statut)
        .doc(id).get() Document spécifique Ponctuel Future Lire un livre quand on en a besoin (page détail)
      6. Méthodes de Mise à Jour Firestore:
        1. MISE À JOUR PARTIELLE - update()

          • Concept : Modifier seulement certains champs d'un document existant
          • MISE À JOUR D'UN SEUL CHAMP
              await FirebaseFirestore.instance
                  .collection('livres')
                  .doc('abc123')              // Document existant
                  .update({
                    'titre': 'Nouveau Titre'  // Seul ce champ sera modifié
                  });
          • MISE À JOUR DE PLUSIEURS CHAMPS
              
              await FirebaseFirestore.instance
                  .collection('livres')
                  .doc('abc123')
                  .update({
                    'titre': 'Titre Modifié',
                    'auteur': 'Nouvel Auteur', 
                    'annee': 2024
                  });
        2. REMPLACEMENT COMPLET -set() sur document existant

          • Concept: Remplacer TOUT le document existant
              
              await FirebaseFirestore.instance
                  .collection('livres')
                  .doc('abc123')  // Document doit EXISTER
                  .set({
                    'titre': 'Nouveau Titre',
                    'auteur': 'Nouvel Auteur',
                    'annee': 2024
                    // Tous les anciens champs sont remplacés
                  });
        3. MISE À JOUR AVEC FUSION - set(merge: true)

          • Concept : Mettre à jour certains champs sans supprimer les autres
              
              await FirebaseFirestore.instance
                  .collection('livres')
                  .doc('abc123')
                  .set({
                    'titre': 'Titre Modifié',
                    'auteur': 'Auteur Modifié'
                  }, SetOptions(merge: true));  // ← Les autres champs sont conservés 
          • Exemple--->AVANT/APRÈS :
              
              {
                "titre": "Ancien Titre", 
                "auteur": "Auteur Original",
                "annee": 2000,
                "genre": "Roman"
              }
              
              // APRÈS set() avec merge: true
              {
                "titre": "Titre Modifié",      // ← Modifié
                "auteur": "Auteur Modifié",    // ← Modifié
                "annee": 2000,                 // ← Conservé !
                "genre": "Roman"               // ← Conservé !
              }
      7. Supprimer un document :
        • Pour supprimer un document, vous pouvez utiliser la méthode 'delete' sur une référence de document. Par exemple :
        • Firestore.instance.collection('collection').document('document').delete();
  9. ? Rechercher des documents dans Firestore

    • Firestore permet d’effectuer des recherches filtrées sur les collections grâce aux requêtes (Query).
    • Ces requêtes servent à extraire uniquement les documents correspondant à certains critères (par exemple : auteur, année, mot-clé…).
      1. Principe général de la recherche dans Firestore
        • Firestore ne fonctionne pas comme une base SQL :
          • Il n’y a pas de SELECT * FROM ... WHERE ... LIKE.
          • À la place, on utilise des requêtes structurées avec la méthode .where().
        • Les requêtes Firestore :
          • Filtrent les documents selon des champs spécifiques,
          • Peuvent être ponctuelles (get()) ou en temps réel (snapshots()),
          • Nécessitent parfois des index pour plusieurs conditions.
      2. Rechercher selon la valeur exacte d'un champ (Rechercher des livres par auteur)
        1. Définition

        2. Cette recherche consiste à filtrer les documents selon la valeur exacte d’un champ.
          Exemple : retrouver tous les livres écrits par Victor Hugo.

          ? Lecture ponctuelle (Future)

          Lecture "one-shot", exécutée une seule fois au moment de l’appel.

          
          Future<List<Livre>> rechercherLivresParAuteur(String auteur) async {
            try {
              final snapshot = await FirebaseFirestore.instance
                  .collection('livres')
                  .where('auteur', isEqualTo: auteur) // Filtre sur le champ 'auteur'
                  .get(); // Lecture ponctuelle
          
              // Conversion des documents Firestore en objets Livre
              return snapshot.docs.map((doc) {
                return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
              }).toList();
            } catch (e) {
              throw 'Erreur lors de la recherche : $e';
            }
          }
          			

          ? Utilisation typique :
          Afficher la liste des livres d’un auteur donné.
          Exemple : l’utilisateur saisit "Victor Hugo" dans un champ de recherche.

          ? Recherche dynamique en temps réel (Stream)

          Lecture en continu, les résultats sont automatiquement mis à jour lorsqu’un changement a lieu dans Firestore.

          
          Stream<List<Livre>> rechercherLivresStream(String auteur) {
            return FirebaseFirestore.instance
                .collection('livres')
                .where('auteur', isEqualTo: auteur)
                .snapshots() // Écoute en temps réel
                .map((snapshot) {
                  return snapshot.docs.map((doc) {
                    return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                  }).toList();
                });
          }
          			

          ? Utilisation typique :
          Rafraîchir automatiquement la liste affichée à l’écran.
          Exemple : affichage avec un StreamBuilder dans Flutter.

      3. Recherche avec plusieurs critères
        1. Définition

        2. Tu peux combiner plusieurs conditions (where) pour affiner la recherche.
          ⚠️ Firestore impose certaines limites (souvent un index combiné est requis).

          ? Exemple : auteur + année minimale

          
          Future<List<Livre>> rechercherLivresAvances(String auteur, int anneeMin) async {
            try {
              final snapshot = await FirebaseFirestore.instance
                  .collection('livres')
                  .where('auteur', isEqualTo: auteur)
                  .where('annee', isGreaterThanOrEqualTo: anneeMin)
                  .get();
          
              return snapshot.docs.map((doc) {
                return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
              }).toList();
            } catch (e) {
              throw 'Erreur lors de la recherche avancée : $e';
            }
          }
          			

          ? Utilisation typique :
          Rechercher tous les livres de Victor Hugo publiés après 1850.
          Utile pour des filtres combinés (par année, catégorie, auteur…).

      4. Recherche par mot-clé (titre ou recherche partielle)
        1. Définition

        2. Firestore ne prend pas en charge les recherches textuelles floues (contains, startsWith, etc.) comme SQL.
          ➡️ Solution : créer un champ d’index de mots-clés (keywords) dans chaque document.

          ? Exemple de structure de document

          
          {
            "titre": "Les Misérables",
            "auteur": "Victor Hugo",
            "annee": 1862,
            "keywords": ["les", "misérables", "les misérables", "victor hugo"]
          }
          			

          Chaque document contient un tableau keywords utilisé pour la recherche.

          ? Exemple de fonction Dart

          
          Future<List<Livre>> rechercherParMotCle(String mot) async {
            try {
              final snapshot = await FirebaseFirestore.instance
                  .collection('livres')
                  .where('keywords', arrayContains: mot.toLowerCase())
                  .get();
          
              return snapshot.docs.map((doc) {
                return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
              }).toList();
            } catch (e) {
              throw 'Erreur lors de la recherche par mot-clé : $e';
            }
          }
          			

          ? Utilisation typique :
          Recherche "intelligente" dans une barre de recherche.
          Exemple : taper “mis” retourne Les Misérables grâce au champ keywords.

      5. Récapitulatif visuel des recherches
      6. Type de recherche Méthode Firestore Mode Retourne Exemple d’usage
        Filtrage simple .where('auteur', isEqualTo: ...) Ponctuel (get()) Future<List> Trouver tous les livres d’un auteur
        Filtrage numérique .where('annee', isGreaterThanOrEqualTo: ...) Ponctuel ou Stream Liste filtrée Livres publiés après une certaine année
        Temps réel .snapshots() Stream Stream<List> Résultats mis à jour automatiquement
        Recherche par mot-clé .where('keywords', arrayContains: ...) Ponctuel Liste filtrée Recherche partielle sur le titre
      7. Activité
        • Ajouter une zone de recherche dans ton interface pour filtrer les livres en temps réel selon le titre saisi.
        • Dans une petite application, il est plus ergonomique d’intégrer la recherche dans la liste principale. Cependant, dans une application plus complexe (ex : librairie en ligne), il est préférable d’avoir une page de recherche dédiée, pour gérer plusieurs filtres et une interface plus riche.
        • Étapes globales
          • Transformer ListeLivresView en StatefulWidget (car la recherche change dynamiquement).
          • Ajouter un TextField (champ de recherche) dans la page.
          • Utiliser livreController.rechercherLivres(query) quand le champ n’est pas vide, sinon afficher la liste complète avec getLivresStream().
  10. ? Sécurité et règles Firestore

    • Les règles Firestore contrôlent qui peut lire et écrire des données.
    • Exemple de règles basiques :
    • rules_version = '2';
      service cloud.firestore {
        match /databases/{database}/documents {
          // RÈGLES POUR DÉVELOPPEMENT (À CHANGER EN PRODUCTION)
          match /livres/{document} {
            allow read, write: if true; // Autorise tout le monde
          }
          
          // OU pour tester avec authentification :
          // match /livres/{document} {
          //   allow read, write: if request.auth != null;
          // }
        }
      }
      
      
  11. Code de l'application

    • models
        livre_model.dart
        class Livre {
          String? id;
          String titre;
          String auteur;
          int annee;
          
          Livre({
            this.id,
            required this.titre,
            required this.auteur,
            required this.annee,
          });
          
          // Convertir Livre en Map pour Firestore
          Map<String, dynamic> toMap() {
            return {
              'titre': titre,
              'auteur': auteur,
              'annee': annee,
            };
          }
          
          // Créer Livre depuis Firestore document
          factory Livre.fromFirestore(Map data, String id) {
            return Livre(
              id: id,
              titre: data['titre'] ?? '',
              auteur: data['auteur'] ?? '',
              annee: data['annee'] ?? 0,
            );
          }
        }
    • controllers
        livre_controller.dart
        import 'package:cloud_firestore/cloud_firestore.dart';
        import '../models/livre_model.dart';
        
        class LivreController {
          final FirebaseFirestore _firestore = FirebaseFirestore.instance;
          
          // Référence à la collection
          CollectionReference get _livresRef => _firestore.collection('livres');
          
          // CREATE - Ajouter un livre
          Future<void> ajouterLivre(Livre livre) async {
            try {
              await _livresRef.add(livre.toMap());
            } catch (e) {
              throw 'Erreur lors de l\'ajout: $e';
            }
          }
          
          // READ - Récupérer tous les livres (Stream)
          Stream<List<Livre>> getLivresStream() {
            return _livresRef.snapshots().map((snapshot) {
              return snapshot.docs.map((doc) {
                return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
              }).toList();
            });
          }
          
          // READ - Récupérer un livre par ID
          Future<Livre?> getLivreById(String id) async {
            try {
              final doc = await _livresRef.doc(id).get();
              if (doc.exists) {
                return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
              }
              return null;
            } catch (e) {
              throw 'Erreur lors de la récupération: $e';
            }
          }
          
          // UPDATE - Modifier un livre
          Future<void> modifierLivre(String id, Livre livre) async {
            try {
              await _livresRef.doc(id).update(livre.toMap());
            } catch (e) {
              throw 'Erreur lors de la modification: $e';
            }
          }
          
          // DELETE - Supprimer un livre
          Future<void> supprimerLivre(String id) async {
            try {
              await _livresRef.doc(id).delete();
            } catch (e) {
              throw 'Erreur lors de la suppression: $e';
            }
          }
          
          // Recherche par titre
          Stream<List<Livre>> rechercherLivres(String query) {
            return _livresRef
                .where('titre', isGreaterThanOrEqualTo: query)
                .where('titre', isLessThan: query + 'z')
                .snapshots()
                .map((snapshot) {
                  return snapshot.docs.map((doc) {
                    return Livre.fromFirestore(doc.data() as Map<String, dynamic>, doc.id);
                  }).toList();
                });
          }
        }
    • views
        ajouter_livre_view.dart
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../controllers/livre_controller.dart';
        import '../models/livre_model.dart';
        
        class AjouterLivreView extends StatefulWidget {
          const AjouterLivreView({super.key});
        
          @override
          State<AjouterLivreView> createState() => _AjouterLivreViewState();
        }
        
        class _AjouterLivreViewState extends State<AjouterLivreView> {
          final _formKey = GlobalKey<FormState>();
          final _titreController = TextEditingController();
          final _auteurController = TextEditingController();
          final _anneeController = TextEditingController();
          
          bool _isLoading = false;
        
          @override
          Widget build(BuildContext context) {
            final livreController = Provider.of<LivreController>(context, listen: false);
        
            return Scaffold(
              appBar: AppBar(
                title: const Text('Ajouter un livre'),
              ),
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(
                        controller: _titreController,
                        decoration: const InputDecoration(
                          labelText: 'Titre',
                          border: OutlineInputBorder(),
                        ),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer un titre';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 16),
                      TextFormField(
                        controller: _auteurController,
                        decoration: const InputDecoration(
                          labelText: 'Auteur',
                          border: OutlineInputBorder(),
                        ),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer un auteur';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 16),
                      TextFormField(
                        controller: _anneeController,
                        decoration: const InputDecoration(
                          labelText: 'Année',
                          border: OutlineInputBorder(),
                        ),
                        keyboardType: TextInputType.number,
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer une année';
                          }
                          final annee = int.tryParse(value);
                          if (annee == null || annee < 0) {
                            return 'Veuillez entrer une année valide';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 24),
                      _isLoading
                          ? const CircularProgressIndicator()
                          : ElevatedButton(
                              onPressed: _submitForm,
                              child: const Text('Ajouter le livre'),
                            ),
                    ],
                  ),
                ),
              ),
            );
          }
          
          Future<void> _submitForm() async {
            if (_formKey.currentState!.validate()) {
              setState(() => _isLoading = true);
              
              final livreController = Provider.of<LivreController>(context, listen: false);
              final nouveauLivre = Livre(
                titre: _titreController.text,
                auteur: _auteurController.text,
                annee: int.parse(_anneeController.text),
              );
              
              try {
                await livreController.ajouterLivre(nouveauLivre);
                Navigator.pop(context);
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Livre ajouté avec succès')),
                );
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              } finally {
                setState(() => _isLoading = false);
              }
            }
          }
          
          @override
          void dispose() {
            _titreController.dispose();
            _auteurController.dispose();
            _anneeController.dispose();
            super.dispose();
          }
        }
        liste_livres_view_recherche.dart
        import '../views/modifier_livre_view.dart';
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../controllers/livre_controller.dart';
        import '../models/livre_model.dart';
        import 'ajouter_livre_view.dart';
        
        class ListeLivresView extends StatefulWidget {
          const ListeLivresView({super.key});
        
          @override
          State<ListeLivresView> createState() => _ListeLivresViewState();
        }
        
        class _ListeLivresViewState extends State<ListeLivresView> {
          String _searchQuery = '';
        
          @override
          Widget build(BuildContext context) {
            final livreController = Provider.of<LivreController>(context, listen: false);
        
            // On choisit le flux en fonction du champ de recherche
            final Stream<List<Livre>> livresStream = _searchQuery.isEmpty
                ? livreController.getLivresStream()
                : livreController.rechercherLivres(_searchQuery);
        
            return Scaffold(
              appBar: AppBar(
                title: const Text('Ma Bibliothèque'),
                actions: [
                  IconButton(
                    icon: const Icon(Icons.add),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const AjouterLivreView()),
                      );
                    },
                  ),
                ],
              ),
              body: Column(
                children: [
                  // ? Zone de recherche
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: TextField(
                      decoration: InputDecoration(
                        prefixIcon: const Icon(Icons.search),
                        hintText: 'Rechercher un livre par titre...',
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                      ),
                      onChanged: (value) {
                        setState(() {
                          _searchQuery = value.trim();
                        });
                      },
                    ),
                  ),
        
                  // ? Liste des livres
                  Expanded(
                    child: StreamBuilder<List<Livre>>(
                      stream: livresStream,
                      builder: (context, snapshot) {
                        if (snapshot.hasError) {
                          return Center(child: Text('Erreur: ${snapshot.error}'));
                        }
        
                        if (snapshot.connectionState == ConnectionState.waiting) {
                          return const Center(child: CircularProgressIndicator());
                        }
        
                        final livres = snapshot.data ?? [];
        
                        if (livres.isEmpty) {
                          return const Center(child: Text('Aucun livre trouvé'));
                        }
        
                        return ListView.builder(
                          itemCount: livres.length,
                          itemBuilder: (context, index) {
                            final livre = livres[index];
                            return LivreTile(livre: livre);
                          },
                        );
                      },
                    ),
                  ),
                ],
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => const AjouterLivreView()),
                  );
                },
                child: const Icon(Icons.add),
              ),
            );
          }
        }
        
        class LivreTile extends StatelessWidget {
          final Livre livre;
        
          const LivreTile({super.key, required this.livre});
        
          @override
          Widget build(BuildContext context) {
            final livreController = Provider.of<LivreController>(context, listen: false);
        
            return Card(
              margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              child: ListTile(
                title: Text(livre.titre),
                subtitle: Text('${livre.auteur} - ${livre.annee}'),
                trailing: PopupMenuButton(
                  onSelected: (value) {
                    switch (value) {
                      case 'modifier':
                         Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ModifierLivreView(livre: livre),
                  ),
                );
                break;
              case 'supprimer':
                _showDeleteDialog(context, livreController, livre.id!);
                break;
            }
          },
                  itemBuilder: (context) => [
          PopupMenuItem(
            value: 'modifier',
            child: Row(
              children: [
                Icon(Icons.edit, color: Color(0xFF4CAF50), size: 20),
                const SizedBox(width: 8),
                const Text('Modifier'),
              ],
            ),
          ),
          PopupMenuItem(
            value: 'supprimer',
            child: Row(
              children: [
                Icon(Icons.delete, color: Colors.red[600], size: 20),
                const SizedBox(width: 8),
                const Text('Supprimer'),
              ],
            ),
          ),
        ],
                ),
              ),
            );
          }
        
          void _showDeleteDialog(
              BuildContext context, LivreController controller, String id) {
            showDialog(
              context: context,
              builder: (context) => AlertDialog(
                title: const Text('Confirmer la suppression'),
                content: const Text('Voulez-vous vraiment supprimer ce livre ?'),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('Annuler'),
                  ),
                  TextButton(
                    onPressed: () async {
                      try {
                        await controller.supprimerLivre(id);
                        Navigator.pop(context);
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text('Livre supprimé avec succès')),
                        );
                      } catch (e) {
                        Navigator.pop(context);
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Erreur: $e')),
                        );
                      }
                    },
                    child: const Text('Supprimer'),
                  ),
                ],
              ),
            );
          }
        }
        liste_livres_view.dart
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../controllers/livre_controller.dart';
        import '../models/livre_model.dart';
        import 'ajouter_livre_view.dart';
        
        class ListeLivresView extends StatelessWidget {
          const ListeLivresView({super.key});
        
          @override
          Widget build(BuildContext context) {
            final livreController = Provider.of<LivreController>(context, listen: false);
        
            return Scaffold(
              appBar: AppBar(
                title: const Text('Ma Bibliothèque'),
                actions: [
                  IconButton(
                    icon: const Icon(Icons.add),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => AjouterLivreView()),
                      );
                    },
                  ),
                ],
              ),
              body: StreamBuilder<List<Livre>>(
                stream: livreController.getLivresStream(),
                builder: (context, snapshot) {
                  if (snapshot.hasError) {
                    return Center(child: Text('Erreur: ${snapshot.error}'));
                  }
                  
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  
                  final livres = snapshot.data ?? [];
                  
                  if (livres.isEmpty) {
                    return const Center(child: Text('Aucun livre trouvé'));
                  }
                  
                  return ListView.builder(
                    itemCount: livres.length,
                    itemBuilder: (context, index) {
                      final livre = livres[index];
                      return LivreTile(livre: livre);
                    },
                  );
                },
              ),
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context) => AjouterLivreView()),
                  );
                },
                child: const Icon(Icons.add),
              ),
            );
          }
        }
        
        class LivreTile extends StatelessWidget {
          final Livre livre;
          
          const LivreTile({super.key, required this.livre});
        
          @override
          Widget build(BuildContext context) {
            final livreController = Provider.of<LivreController>(context, listen: false);
        
            return Card(
              margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              child: ListTile(
                title: Text(livre.titre),
                subtitle: Text('${livre.auteur} - ${livre.annee}'),
                trailing: PopupMenuButton(
                  onSelected: (value) {
                    switch (value) {
                      case 'modifier':
                        // Navigation vers modification
                        break;
                      case 'supprimer':
                        _showDeleteDialog(context, livreController, livre.id!);
                        break;
                    }
                  },
                  itemBuilder: (context) => [
                    const PopupMenuItem(value: 'modifier', child: Text('Modifier')),
                    const PopupMenuItem(value: 'supprimer', child: Text('Supprimer')),
                  ],
                ),
              ),
            );
          }
          
          void _showDeleteDialog(BuildContext context, LivreController controller, String id) {
            showDialog(
              context: context,
              builder: (context) => AlertDialog(
                title: const Text('Confirmer la suppression'),
                content: const Text('Voulez-vous vraiment supprimer ce livre ?'),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('Annuler'),
                  ),
                  TextButton(
                    onPressed: () async {
                      try {
                        await controller.supprimerLivre(id);
                        Navigator.pop(context);
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text('Livre supprimé avec succès')),
                        );
                      } catch (e) {
                        Navigator.pop(context);
                        ScaffoldMessenger.of(context).showSnackBar(
                          SnackBar(content: Text('Erreur: $e')),
                        );
                      }
                    },
                    child: const Text('Supprimer'),
                  ),
                ],
              ),
            );
          }
        }
        modifier_livre_view.dart
        import 'package:flutter/material.dart';
        import 'package:provider/provider.dart';
        import '../controllers/livre_controller.dart';
        import '../models/livre_model.dart';
        
        class ModifierLivreView extends StatefulWidget {
          final Livre livre;
          
          const ModifierLivreView({
            super.key,
            required this.livre,
          });
        
          @override
          State<ModifierLivreView> createState() => _ModifierLivreViewState();
        }
        
        class _ModifierLivreViewState extends State<ModifierLivreView> {
          final _formKey = GlobalKey<FormState>();
          late TextEditingController _titreController;
          late TextEditingController _auteurController;
          late TextEditingController _anneeController;
          
          bool _isLoading = false;
        
          @override
          void initState() {
            super.initState();
            // Initialiser les contrôleurs avec les valeurs existantes du livre
            _titreController = TextEditingController(text: widget.livre.titre);
            _auteurController = TextEditingController(text: widget.livre.auteur);
            _anneeController = TextEditingController(text: widget.livre.annee.toString());
          }
        
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              appBar: AppBar(
                title: const Text('Modifier le livre'),
                actions: [
                  IconButton(
                    icon: const Icon(Icons.delete, color: Colors.red),
                    onPressed: () => _showDeleteDialog(context),
                  ),
                ],
              ),
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(
                        controller: _titreController,
                        decoration: const InputDecoration(
                          labelText: 'Titre',
                          border: OutlineInputBorder(),
                        ),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer un titre';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 16),
                      TextFormField(
                        controller: _auteurController,
                        decoration: const InputDecoration(
                          labelText: 'Auteur',
                          border: OutlineInputBorder(),
                        ),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer un auteur';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 16),
                      TextFormField(
                        controller: _anneeController,
                        decoration: const InputDecoration(
                          labelText: 'Année',
                          border: OutlineInputBorder(),
                        ),
                        keyboardType: TextInputType.number,
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Veuillez entrer une année';
                          }
                          final annee = int.tryParse(value);
                          if (annee == null || annee < 0) {
                            return 'Veuillez entrer une année valide';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 24),
                      Row(
                        children: [
                          Expanded(
                            child: OutlinedButton(
                              onPressed: _isLoading ? null : () => Navigator.pop(context),
                              child: const Text('Annuler'),
                            ),
                          ),
                          const SizedBox(width: 16),
                          Expanded(
                            child: _isLoading
                                ? const Center(child: CircularProgressIndicator())
                                : ElevatedButton(
                                    onPressed: _submitForm,
                                    child: const Text('Modifier'),
                                  ),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            );
          }
          
          Future<void> _submitForm() async {
            if (_formKey.currentState!.validate()) {
              setState(() => _isLoading = true);
              
              final livreController = Provider.of<LivreController>(context, listen: false);
              
              // Créer un livre mis à jour avec le même ID
              final livreModifie = Livre(
                id: widget.livre.id, // Garder le même ID
                titre: _titreController.text,
                auteur: _auteurController.text,
                annee: int.parse(_anneeController.text),
              );
              
              try {
                // Appel à votre méthode modifierLivre qui prend l'ID et le livre
                await livreController.modifierLivre(livreModifie.id!, livreModifie);
                Navigator.pop(context);
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Livre modifié avec succès')),
                );
              } catch (e) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('Erreur: $e')),
                );
              } finally {
                setState(() => _isLoading = false);
              }
            }
          }
        
          void _showDeleteDialog(BuildContext context) {
            showDialog(
              context: context,
              builder: (BuildContext context) {
                return AlertDialog(
                  title: const Text('Supprimer le livre'),
                  content: const Text('Êtes-vous sûr de vouloir supprimer ce livre ?'),
                  actions: [
                    TextButton(
                      onPressed: () => Navigator.pop(context),
                      child: const Text('Annuler'),
                    ),
                    TextButton(
                      onPressed: () async {
                        Navigator.pop(context); // Fermer la dialog
                        await _deleteLivre(context);
                      },
                      child: const Text(
                        'Supprimer',
                        style: TextStyle(color: Colors.red),
                      ),
                    ),
                  ],
                );
              },
            );
          }
        
          Future<void> _deleteLivre(BuildContext context) async {
            setState(() => _isLoading = true);
            
            final livreController = Provider.of<LivreController>(context, listen: false);
            
            try {
              await livreController.supprimerLivre(widget.livre.id!);
              Navigator.pop(context); // Retour à la liste
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Livre supprimé avec succès')),
              );
            } catch (e) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Erreur: $e')),
              );
            } finally {
              setState(() => _isLoading = false);
            }
          }
          
          @override
          void dispose() {
            _titreController.dispose();
            _auteurController.dispose();
            _anneeController.dispose();
            super.dispose();
          }
        }
      main.dart
      import 'package:flutter/material.dart';
      import 'package:firebase_core/firebase_core.dart';
      import 'package:provider/provider.dart';
      import 'firebase_options.dart'; // ← Fichier de configuration
      import 'controllers/livre_controller.dart';
      //import 'views/liste_livres_view.dart';
      import 'views/liste_livres_view_recherche.dart';
      
      void main() async {
        WidgetsFlutterBinding.ensureInitialized();
        
        // Initialisation Firebase avec la configuration spécifique
        await Firebase.initializeApp(
          options: DefaultFirebaseOptions.currentPlatform, // ← Utilise notre config
        );
        
        runApp(MyApp());
      }
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MultiProvider(
            providers: [
              Provider<LivreController>(
                create: (_) => LivreController(),
              ),
            ],
            child: MaterialApp(
              title: 'Ma Bibliothèque',
               // theme: ThemeData(
              //   primarySwatch: Colors.blue,
              //   useMaterial3: true,
              // ),
              theme: ThemeData(
                useMaterial3: false, // ⚠️ Pour compatibilité avec cardTheme comme ci-dessous
                primaryColor: const Color(0xFF4CAF50),
                scaffoldBackgroundColor: const Color(0xFFF5F5F5),
                appBarTheme: const AppBarTheme(
                  backgroundColor: Color(0xFF4CAF50),
                  foregroundColor: Colors.white,
                ),
                floatingActionButtonTheme: const FloatingActionButtonThemeData(
                  backgroundColor: Color(0xFF4CAF50),
                ),
                cardTheme: CardThemeData( // ← ICI : CardThemeData au lieu de CardTheme ancienne version
                  color: Colors.white,
                  elevation: 4,
                  shadowColor: Colors.black12,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.all(Radius.circular(12)),
                  ),
                ),
                inputDecorationTheme: InputDecorationTheme(
                  filled: true,
                  fillColor: Colors.white,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                    borderSide: BorderSide.none,
                  ),
                ),
                popupMenuTheme: PopupMenuThemeData(
        color: Colors.white,
        elevation: 8,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
          side: BorderSide(color: Colors.grey[300]!, width: 1),
        ),
        textStyle: TextStyle(
          color: Colors.grey[800],
          fontSize: 14,
          fontWeight: FontWeight.w500,
        ),
       
      ),
              ),
              home: const ListeLivresView(),
              debugShowCheckedModeBanner: false,
            ),
          );
        }
      }