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
Introduction à Cloud Firestore
- Qu'est-ce que Cloud Firestore?
Cloud Firestoreest une base de données NoSQL cloud, temps réel et scalable développée par Google.Cloud Firestoreest 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.
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
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
- 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.
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.', ); } } }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, ), ); } }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.
- À 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.
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, ); } }-
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.
- À 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.
Fichier
livre_controller.dartimport '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(); }); } } -
VUE (views/ajouter_livre.dart)
- Rôle : Afficher l’interface et récupérer les données de l’utilisateur.
- À 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: 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(); } }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'), ), ], ), ); } }
Utilisateur ➜ View (Formulaire) ➜ Controller ➜ Model ➜ Firestore ⬆︎ ⬆︎ ⬆︎ Affichage Logique MÉTIER Données stockéesProjet à 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
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
- Créer une application mobile permettant de gérer une collection de livres avec :
Configuration du projet Flutter avec Firestore
-
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()); } -
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; // } } }
-
-
Effectuer des opérations
CRUDdans 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.-
Règles et bonnes pratiques pour créer un document dans Firestore :
-
Utiliser des modèles (classes) pour représenter les données
- Toujours créer une classe Livre (ou autre) pour représenter un document :
- Avantage: cohérence, lisibilité du code, moins d'erreurs lors du stockage.
class Livre { final String titre; final String auteur; final int annee; Livre({required this.titre, required this.auteur, required this.annee}); MaptoMap() { return { 'titre': titre, 'auteur': auteur, 'annee': annee, }; } } -
Utiliser
add()oudoc().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());
- Toujours gérer les exceptions
- Exemple :
Important : ne jamais laisser une erreur Firestore passer en silence.try { await _livresRef.add(livre.toMap()); } catch (e) { print("Erreur lors de l'ajout: $e"); }
-
-
- Firestore vous permet d'effectuer des opérations de création, de lecture, de mise à jour et de suppression
(
-
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
-
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.
- 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).
Stream- , doc.id);
}).toList();
});
}
- En temps réel (Stream)
- Lecture ponctuelle (Future)
- Lecture "
one shot", uniquement au moment où le code est exécuté. - Utilisation typique :
- Charger une liste statique, non mise à jour automatiquement.
- Exemple: Exporter une liste d’objets dans un fichier Excel.
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'; } } - Lecture "
-
Lire un document spécifique via son ID
- En temps réel (Stream)
- Pour suivre les changements en direct sur un seul document.
- Utilisation typique :
- Suivre l'état d’un document en particulier (ex : stock en temps réel, statut de commande).
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; }); } - 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. - Utilisation typique :
- Récupérer les détails d'un élément lors d’une navigation vers une nouvelle page.
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'; } }
- En temps réel (Stream)
-
Récapitulatif visuel
-
Méthodes de Mise à Jour Firestore:
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 });
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 });
- Concept: Remplacer TOUT le document existant
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é ! }
- Concept : Mettre à jour certains champs sans supprimer les autres
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();
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)
- Firestore propose 2 niveaux de lecture :
? 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é…).
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().
- Il n’y a pas de
- 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.
- Firestore ne fonctionne pas comme une base SQL :
Rechercher selon la valeur exacte d'un champ (Rechercher des livres par auteur)
Définition
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 unStreamBuilderdans Flutter.Recherche avec plusieurs critères
Définition
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…).Recherche par mot-clé (titre ou recherche partielle)
Définition
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
keywordsutilisé 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 champkeywords.Récapitulatif visuel des recherches
-
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
ListeLivresViewenStatefulWidget(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 avecgetLivresStream().
- Transformer
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
- Firestore permet d’effectuer des recherches filtrées sur les collections grâce aux requêtes (
? 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; // } } }Code de l'application
modelslivre_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(Mapdata, String id) { return Livre( id: id, titre: data['titre'] ?? '', auteur: data['auteur'] ?? '', annee: data['annee'] ?? 0, ); } } controllerslivre_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(); }); } }viewsajouter_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, ), ); } }