↑
CHAPITRE 5.6

Atelier pratique : Application CRUD avec Firebase

Implémenter étape par étape une application complète avec authentification et gestion CRUD
Dans cet atelier, vous allez construire étape par étape une application complète avec Firebase : authentification (inscription/connexion) et gestion CRUD de personnes (nom, prénom, âge) stockées dans Cloud Firestore. Chaque étape ajoute une fonctionnalité précise. À la fin, vous aurez une application fonctionnelle prête à être utilisée.

5.6Atelier pratique : Application CRUD avec Firebase

Objectif de l'atelier :
Créer une application Flutter avec :
  • Authentification Firebase (inscription et connexion)
  • Gestion CRUD complète de personnes (Create, Read, Update, Delete)
  • Synchronisation en temps rĂ©el avec Firestore
  • Interface utilisateur complète et fonctionnelle

Étape 1 : Configuration initiale

1.1 Créer le projet Flutter

Créez un nouveau projet Flutter :

flutter create first_app
cd first_app

1.2 Ajouter les dépendances

Ouvrez pubspec.yaml et ajoutez les packages Firebase :

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2
  firebase_auth: ^4.16.0
  cloud_firestore: ^4.14.0

Exécutez flutter pub get pour installer les packages.

1.3 Configurer Firebase

Suivez les étapes de configuration Firebase (comme dans le chapitre 5.5.2) :

  • CrĂ©ez un projet dans Firebase Console
  • Activez Authentication (Email/Password)
  • Activez Cloud Firestore (mode test pour commencer)
  • TĂ©lĂ©chargez google-services.json (Android) et GoogleService-Info.plist (iOS)
  • Placez-les dans les dossiers appropriĂ©s et configurez les fichiers build.gradle
Structure Firestore :
Nous utiliserons une collection personnes avec les champs : nom, prenom, age, et createdAt.

Étape 2 : Structure de base (main.dart)

2.1 Initialiser Firebase

Remplacez le contenu de lib/main.dart par :

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: AuthGate(),
    );
  }
}

2.2 Créer AuthGate

Ajoutez la classe AuthGate qui gère la navigation selon l'état d'authentification :

class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<User?>(
      stream: FirebaseAuth.instance.authStateChanges(),
      builder: (context, snapshot) {
        // En attente de vérification
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }

        // Utilisateur connecté → afficher la page principale
        if (snapshot.hasData) {
          return const PersonnesPage(); // À créer à l'étape 5
        }

        // Utilisateur non connecté → afficher la page de connexion
        return const LoginPage(); // À créer à l'étape 3
      },
    );
  }
}
AuthGate :
Cette classe utilise StreamBuilder sur authStateChanges() pour détecter automatiquement si un utilisateur est connecté. Si oui, elle affiche PersonnesPage ; sinon, LoginPage.

Étape 3 : Page de connexion (LoginPage)

3.1 Créer la classe LoginPage

Ajoutez cette classe dans main.dart :

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  bool loading = false;

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Firebase Auth')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Mot de passe',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 24),
            if (loading)
              const CircularProgressIndicator()
            else
              Row(
                children: [
                  Expanded(
                    child: ElevatedButton(
                      onPressed: signup,
                      child: const Text("S'inscrire"),
                    ),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: ElevatedButton(
                      onPressed: login,
                      child: const Text('Se connecter'),
                    ),
                  ),
                ],
              ),
          ],
        ),
      ),
    );
  }
}

3.2 Ajouter les méthodes login() et signup()

Ajoutez ces méthodes dans la classe _LoginPageState :

  Future<void> login() async {
    setState(() => loading = true);
    try {
      await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: emailController.text.trim(),
        password: passwordController.text,
      );
      // Pas besoin de Navigator : AuthGate gère automatiquement la navigation
    } on FirebaseAuthException catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.message ?? 'Erreur de connexion')),
      );
    } finally {
      if (mounted) setState(() => loading = false);
    }
  }

  Future<void> signup() async {
    setState(() => loading = true);
    try {
      await FirebaseAuth.instance.createUserWithEmailAndPassword(
        email: emailController.text.trim(),
        password: passwordController.text,
      );
      // Pas besoin de Navigator : AuthGate gère automatiquement la navigation
    } on FirebaseAuthException catch (e) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(e.message ?? 'Erreur d\'inscription')),
      );
    } finally {
      if (mounted) setState(() => loading = false);
    }
  }
Pourquoi pas de Navigator ?
Comme AuthGate écoute authStateChanges(), dès qu'un utilisateur se connecte ou s'inscrit, le StreamBuilder détecte le changement et affiche automatiquement PersonnesPage. C'est plus élégant que de gérer manuellement la navigation !

3.3 Tester l'authentification

Lancez l'application. Vous devriez voir la page de connexion. Testez l'inscription avec un email et un mot de passe (minimum 6 caractères). Après inscription, vous serez automatiquement redirigé vers PersonnesPage (que nous créerons à l'étape 5).


Étape 4 : Page d'accueil temporaire (optionnel)

Pour tester que l'authentification fonctionne avant de créer la page CRUD, vous pouvez créer une page d'accueil simple :

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final user = FirebaseAuth.instance.currentUser;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Accueil'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
              // AuthGate gère automatiquement le retour à LoginPage
            },
          )
        ],
      ),
      body: Center(
        child: Text(
          'Connecté en tant que :\n${user?.email}',
          textAlign: TextAlign.center,
          style: const TextStyle(fontSize: 18),
        ),
      ),
    );
  }
}

Remplacez temporairement PersonnesPage() par HomePage() dans AuthGate pour tester. Une fois la page CRUD créée, vous remplacerez HomePage par PersonnesPage.


Étape 5 : Créer le fichier personnepage.dart

5.1 Créer le fichier

Créez un nouveau fichier lib/personnepage.dart :

5.2 Structure de base

Commencez par la structure de base de la classe :

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

class PersonnesPage extends StatefulWidget {
  const PersonnesPage({super.key});

  @override
  State<PersonnesPage> createState() => _PersonnesPageState();
}

class _PersonnesPageState extends State<PersonnesPage> {
  // ContrĂ´leurs pour les champs de formulaire
  final nomController = TextEditingController();
  final prenomController = TextEditingController();
  final ageController = TextEditingController();

  // Référence à la collection Firestore
  final CollectionReference personnes =
      FirebaseFirestore.instance.collection('personnes');

  // ID du document en cours d'édition (null = création, non null = modification)
  String? editingId;

  @override
  void dispose() {
    nomController.dispose();
    prenomController.dispose();
    ageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('CRUD Personnes'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
            },
          ),
        ],
      ),
      body: const Center(
        child: Text('Page Personnes - À compléter'),
      ),
    );
  }
}
editingId :
Cette variable permet de savoir si on est en mode "création" (editingId == null) ou "modification" (editingId != null). Cela permet de réutiliser le même formulaire pour les deux opérations.

Étape 6 : CREATE - Ajouter une personne

6.1 Méthode ajouterPersonne()

Ajoutez cette méthode dans _PersonnesPageState :

  Future<void> ajouterPersonne() async {
    if (nomController.text.isEmpty || 
        prenomController.text.isEmpty || 
        ageController.text.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Veuillez remplir tous les champs')),
      );
      return;
    }

    try {
      await personnes.add({
        'nom': nomController.text,
        'prenom': prenomController.text,
        'age': int.parse(ageController.text),
        'createdAt': Timestamp.now(),
      });
      resetForm();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

  void resetForm() {
    nomController.clear();
    prenomController.clear();
    ageController.clear();
    setState(() => editingId = null);
  }

6.2 Ajouter le formulaire dans build()

Remplacez le body de Scaffold par :

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('CRUD Personnes'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Formulaire
            TextField(
              controller: nomController,
              decoration: const InputDecoration(
                labelText: 'Nom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: prenomController,
              decoration: const InputDecoration(
                labelText: 'Prénom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: ageController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'Âge',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            // Bouton Ajouter
            ElevatedButton(
              onPressed: ajouterPersonne,
              child: const Text('Ajouter'),
            ),
          ],
        ),
      ),
    );
  }

6.3 Tester l'ajout

Lancez l'application, connectez-vous, et testez l'ajout d'une personne. Vérifiez dans la console Firebase que le document a bien été créé dans la collection personnes.


Étape 7 : READ - Afficher la liste des personnes

7.1 Ajouter StreamBuilder pour la liste

Remplacez le body pour ajouter la liste sous le formulaire :

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('CRUD Personnes'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            // Formulaire (identique à l'étape 6)
            TextField(
              controller: nomController,
              decoration: const InputDecoration(
                labelText: 'Nom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: prenomController,
              decoration: const InputDecoration(
                labelText: 'Prénom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: ageController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'Âge',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: ajouterPersonne,
              child: const Text('Ajouter'),
            ),
            const Divider(),
            // Liste des personnes
            Expanded(
              child: StreamBuilder<QuerySnapshot>(
                stream: personnes.orderBy('createdAt', descending: true).snapshots(),
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  if (snapshot.hasError) {
                    return Center(child: Text('Erreur : ${snapshot.error}'));
                  }
                  if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
                    return const Center(child: Text('Aucune personne enregistrée'));
                  }

                  return ListView.builder(
                    itemCount: snapshot.data!.docs.length,
                    itemBuilder: (context, index) {
                      final doc = snapshot.data!.docs[index];
                      final data = doc.data() as Map<String, dynamic>;

                      return Card(
                        margin: const EdgeInsets.symmetric(vertical: 4),
                        child: ListTile(
                          title: Text('${data['prenom']} ${data['nom']}'),
                          subtitle: Text('Âge : ${data['age']}'),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

7.2 Tester l'affichage

Ajoutez quelques personnes et vérifiez qu'elles s'affichent automatiquement dans la liste. La synchronisation en temps réel fonctionne grâce à snapshots() : si vous ajoutez une personne, elle apparaît immédiatement sans rechargement.


Étape 8 : UPDATE - Modifier une personne

8.1 Méthode modifierPersonne()

Ajoutez cette méthode :

  Future<void> modifierPersonne() async {
    if (editingId == null) return;

    if (nomController.text.isEmpty || 
        prenomController.text.isEmpty || 
        ageController.text.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Veuillez remplir tous les champs')),
      );
      return;
    }

    try {
      await personnes.doc(editingId).update({
        'nom': nomController.text,
        'prenom': prenomController.text,
        'age': int.parse(ageController.text),
      });
      resetForm();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

8.2 Ajouter le bouton Modifier dans la liste

Modifiez le ListTile dans la liste pour ajouter un bouton "Modifier" :

                      return Card(
                        margin: const EdgeInsets.symmetric(vertical: 4),
                        child: ListTile(
                          title: Text('${data['prenom']} ${data['nom']}'),
                          subtitle: Text('Âge : ${data['age']}'),
                          trailing: IconButton(
                            icon: const Icon(Icons.edit),
                            onPressed: () {
                              setState(() {
                                editingId = doc.id;
                                nomController.text = data['nom'] ?? '';
                                prenomController.text = data['prenom'] ?? '';
                                ageController.text = (data['age'] ?? 0).toString();
                              });
                            },
                          ),
                        ),
                      );

8.3 Modifier le bouton d'action

Remplacez le bouton "Ajouter" par un bouton qui change selon le mode :

            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: editingId == null ? ajouterPersonne : modifierPersonne,
                    child: Text(editingId == null ? 'Ajouter' : 'Modifier'),
                  ),
                ),
                if (editingId != null) ...[
                  const SizedBox(width: 8),
                  Expanded(
                    child: OutlinedButton(
                      onPressed: resetForm,
                      child: const Text('Annuler'),
                    ),
                  ),
                ],
              ],
            ),

8.4 Tester la modification

Cliquez sur l'icône "Modifier" d'une personne. Les champs du formulaire se remplissent. Modifiez les valeurs et cliquez sur "Modifier". La liste se met à jour automatiquement grâce à snapshots().


Étape 9 : DELETE - Supprimer une personne

9.1 Méthode supprimerPersonne()

Ajoutez cette méthode :

  Future<void> supprimerPersonne(String id) async {
    try {
      await personnes.doc(id).delete();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

9.2 Ajouter le bouton Supprimer

Modifiez le trailing du ListTile pour ajouter un bouton "Supprimer" :

                          trailing: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              IconButton(
                                icon: const Icon(Icons.edit),
                                onPressed: () {
                                  setState(() {
                                    editingId = doc.id;
                                    nomController.text = data['nom'] ?? '';
                                    prenomController.text = data['prenom'] ?? '';
                                    ageController.text = (data['age'] ?? 0).toString();
                                  });
                                },
                              ),
                              IconButton(
                                icon: const Icon(Icons.delete, color: Colors.red),
                                onPressed: () => supprimerPersonne(doc.id),
                              ),
                            ],
                          ),

9.3 Tester la suppression

Cliquez sur l'icône "Supprimer" (rouge) d'une personne. Elle disparaît immédiatement de la liste grâce à la synchronisation en temps réel.


Étape 10 : Finalisation et améliorations

10.1 Importer personnepage.dart dans main.dart

Ajoutez l'import en haut de main.dart :

import 'package:first_app/personnepage.dart';

10.2 Code final complet

Voici le code final de personnepage.dart :

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

class PersonnesPage extends StatefulWidget {
  const PersonnesPage({super.key});

  @override
  State<PersonnesPage> createState() => _PersonnesPageState();
}

class _PersonnesPageState extends State<PersonnesPage> {
  final nomController = TextEditingController();
  final prenomController = TextEditingController();
  final ageController = TextEditingController();

  final CollectionReference personnes =
      FirebaseFirestore.instance.collection('personnes');

  String? editingId;

  Future<void> ajouterPersonne() async {
    if (nomController.text.isEmpty || 
        prenomController.text.isEmpty || 
        ageController.text.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Veuillez remplir tous les champs')),
      );
      return;
    }

    try {
      await personnes.add({
        'nom': nomController.text,
        'prenom': prenomController.text,
        'age': int.parse(ageController.text),
        'createdAt': Timestamp.now(),
      });
      resetForm();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

  Future<void> modifierPersonne() async {
    if (editingId == null) return;

    if (nomController.text.isEmpty || 
        prenomController.text.isEmpty || 
        ageController.text.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Veuillez remplir tous les champs')),
      );
      return;
    }

    try {
      await personnes.doc(editingId).update({
        'nom': nomController.text,
        'prenom': prenomController.text,
        'age': int.parse(ageController.text),
      });
      resetForm();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

  Future<void> supprimerPersonne(String id) async {
    try {
      await personnes.doc(id).delete();
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Erreur : $e')),
        );
      }
    }
  }

  void resetForm() {
    nomController.clear();
    prenomController.clear();
    ageController.clear();
    setState(() => editingId = null);
  }

  @override
  void dispose() {
    nomController.dispose();
    prenomController.dispose();
    ageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('CRUD Personnes'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: nomController,
              decoration: const InputDecoration(
                labelText: 'Nom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: prenomController,
              decoration: const InputDecoration(
                labelText: 'Prénom',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 12),
            TextField(
              controller: ageController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'Âge',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: editingId == null ? ajouterPersonne : modifierPersonne,
                    child: Text(editingId == null ? 'Ajouter' : 'Modifier'),
                  ),
                ),
                if (editingId != null) ...[
                  const SizedBox(width: 8),
                  Expanded(
                    child: OutlinedButton(
                      onPressed: resetForm,
                      child: const Text('Annuler'),
                    ),
                  ),
                ],
              ],
            ),
            const Divider(),
            Expanded(
              child: StreamBuilder<QuerySnapshot>(
                stream: personnes.orderBy('createdAt', descending: true).snapshots(),
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                  }
                  if (snapshot.hasError) {
                    return Center(child: Text('Erreur : ${snapshot.error}'));
                  }
                  if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
                    return const Center(child: Text('Aucune personne enregistrée'));
                  }

                  return ListView.builder(
                    itemCount: snapshot.data!.docs.length,
                    itemBuilder: (context, index) {
                      final doc = snapshot.data!.docs[index];
                      final data = doc.data() as Map<String, dynamic>;

                      return Card(
                        margin: const EdgeInsets.symmetric(vertical: 4),
                        child: ListTile(
                          title: Text('${data['prenom']} ${data['nom']}'),
                          subtitle: Text('Âge : ${data['age']}'),
                          trailing: Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              IconButton(
                                icon: const Icon(Icons.edit),
                                onPressed: () {
                                  setState(() {
                                    editingId = doc.id;
                                    nomController.text = data['nom'] ?? '';
                                    prenomController.text = data['prenom'] ?? '';
                                    ageController.text = (data['age'] ?? 0).toString();
                                  });
                                },
                              ),
                              IconButton(
                                icon: const Icon(Icons.delete, color: Colors.red),
                                onPressed: () => supprimerPersonne(doc.id),
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

10.3 Améliorations possibles

  • Validation : Ajouter une validation pour l'âge (nombre positif, raisonnable)
  • Confirmation de suppression : Afficher un dialog de confirmation avant de supprimer
  • Recherche : Ajouter un champ de recherche pour filtrer les personnes
  • Tri : Permettre de trier par nom, prĂ©nom ou âge
  • Règles Firestore : Configurer les règles de sĂ©curitĂ© pour limiter l'accès
Félicitations !
Vous avez créé une application CRUD complète avec Firebase ! Cette application démontre :
  • Authentification Firebase (inscription/connexion)
  • CRUD complet (Create, Read, Update, Delete)
  • Synchronisation en temps rĂ©el avec Firestore
  • Interface utilisateur fonctionnelle