5.6Atelier pratique : Application CRUD avec Firebase
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) etGoogleService-Info.plist(iOS) - Placez-les dans les dossiers appropriés et configurez les fichiers
build.gradle
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
},
);
}
}
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);
}
}
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'),
),
);
}
}
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
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