À propos de ce cours
Configurer Cloud Firestore dans Flutter : configuration manuelle, CRUD et recherche
1. Objectifs et projet fil rouge
À la fin de cette leçon, vous serez capable de :
- relier à la main une application Flutter Android à un projet Firebase, sans outil en ligne de commande ;
- initialiser Firebase de deux façons : avec
google-services.jsonseul, ou avec un fichierfirebase_options.dart; - ajouter, lire, modifier et supprimer des documents dans Cloud Firestore, avec deux méthodes pour chaque opération ;
- lire un document par son identifiant et lire tous les documents d'une collection ;
- écrire des recherches : égalité, comparaison, tri, « commence par » et conditions multiples.
Comment utiliser cette leçon : les exemples du cours utilisent l'application « Gestion des stagiaires » (collection stagiaires). Les activités vous font construire votre propre application, étape par étape : elles posent des questions qui vous guident, sans donner les réponses.
Projet fil rouge des activités : « FormaApp », le catalogue des formations du centre.
• Collection
formations: intitulé, spécialité, niveau (CAP, BTP, BTS), durée en mois, nombre de places.• L'application liste les formations, affiche la fiche d'une formation, permet d'en ajouter, de les modifier, de les supprimer et de les rechercher.
Les champs et les écrans sont différents de l'exemple du cours : vous devez adapter le code, pas le recopier.
2. Cloud Firestore en bref
Cloud Firestore est la base de données NoSQL de Firebase. Les données ne sont pas rangées dans des tables mais dans des collections de documents, chaque document contenant des champs (texte, nombre, booléen, date, tableau, objet…).
| SQL (MySQL) | Firestore | Exemple |
|---|---|---|
| Table | Collection | stagiaires |
| Ligne | Document | le stagiaire S042 |
| Clé primaire | Identifiant du document | S042 ou Xk3aP9q2LmZ8 (automatique) |
| Colonne | Champ | nom, moyenne |
SELECT … WHERE |
where(...).get() |
stagiaires du groupe DEV-101 |
- Deux documents d'une même collection peuvent avoir des champs différents : il n'y a pas de schéma imposé. C'est souple, mais c'est à votre code de rester cohérent.
- Firestore peut prévenir l'application en temps réel quand une donnée change : c'est la différence majeure avec une API classique.
- Firestore facture au nombre de lectures, d'écritures et de suppressions de documents (avec un quota gratuit quotidien suffisant pour les TP).

Figure 1 — Une collection, ses documents et leurs champs ; à droite, les opérateurs de recherche (partie 11).
3. Configuration Android, étape par étape
La configuration se fait entièrement à la main, dans la console Firebase et dans les fichiers du projet. La figure 2 résume les fichiers à modifier : les numéros correspondent aux étapes de cette partie et de la suivante.

Figure 2 — Les 6 fichiers et les 7 étapes de la configuration manuelle.
3.1 Étape 1 : relever le nom du package
Firebase identifie votre application Android par son nom de package. Ouvrez android/app/build.gradle.kts et relevez la valeur de applicationId:
android {
namespace = "tn.atfp.gestion_stagiaires"
// ...
defaultConfig {
applicationId = "tn.atfp.gestion_stagiaires" // ← à recopier dans Firebase
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
// ...
}
}
- Sur un projet ancien, le fichier s'appelle
android/app/build.gradle(sans.kts) et la ligne s'écritapplicationId "tn.atfp.gestion_stagiaires". - Si vous voulez changer ce nom, faites-le maintenant, avant de l'enregistrer dans Firebase :
applicationIdetnamespace.
3.2 Étape 2 : créer le projet Firebase et télécharger google-services.json
- Ouvrez console.firebase.google.com et créez un projet (Google Analytics n'est pas nécessaire pour ce TP).
- Sur la page d'accueil du projet, cliquez sur l'icône Android pour ajouter une application.
- Collez le nom du package relevé à l'étape 1 (exactement le même). Le surnom et l'empreinte SHA-1 sont facultatifs pour Firestore.
- Téléchargez le fichier
google-services.jsonet placez-le dans le dossierandroid/app/. - Ignorez les instructions Gradle affichées par la console à l'étape suivante de l'assistant : elles visent les projets Android natifs. Nous les adaptons ci-dessous (étapes 3 et 4).
Le fichier doit s'appeler exactement
google-services.json (sans « (1) » ajouté par le navigateur) et se trouver dans android/app/, pas dans android/ ni à la racine du projet. Vérifiez à l'intérieur que "package_name" est bien votre applicationId. 3.3 Étape 3 : déclarer le plugin Google Services
Le plugin Gradle com.google.gms.google-services lit google-services.json pendant la compilation. On le déclare une fois pour tout le projet. Choisissez la méthode qui correspond à vos fichiers :
Méthode 1 Projet récent (fichiers .kts, Flutter 3.29 et plus) : android/settings.gradle.kts
Ajoutez une ligne dans le bloc plugins:
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.gms.google-services") version "4.4.4" apply false // ← à ajouter
}
Gardez les versions déjà présentes dans votre fichier pour les autres plugins : seule la ligne google-services est à ajouter.
Méthode 2 Projet ancien (fichiers Groovy .gradle) : android/build.gradle
Les projets créés avant 2024 utilisent souvent un bloc buildscript:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.google.gms:google-services:4.4.4" // ← à ajouter
}
}
N'utilisez qu'une seule des deux méthodes, celle qui correspond aux fichiers de votre projet. Mélanger
buildscript et le bloc plugins provoque des erreurs « plugin déjà déclaré ».3.4 Étape 4 : appliquer le plugin à l'application
Méthode 1 Projet récent : android/app/build.gradle.kts
plugins {
id("com.android.application")
id("kotlin-android")
id("com.google.gms.google-services") // ← à ajouter (sans version)
// Le plugin Flutter doit rester le dernier
id("dev.flutter.flutter-gradle-plugin")
}
Méthode 2 Projet ancien : android/app/build.gradle
plugins {
id "com.android.application"
id "kotlin-android"
id "com.google.gms.google-services" // ← à ajouter
id "dev.flutter.flutter-gradle-plugin"
}
// ou, dans les projets très anciens, tout en bas du fichier :
// apply plugin: 'com.google.gms.google-services'
3.5 Étape 5 : ajouter les packages et vérifier minSdk
Ouvrez pubspec.yaml et ajoutez les deux packages dans dependencies (attention à l'indentation : 2 espaces). Enregistrez : VS Code et Android Studio lancent alors Pub get automatiquement (sinon, cliquez sur Pub get en haut du fichier).
dependencies:
flutter:
sdk: flutter
firebase_core: ^4.2.1 # obligatoire : connexion à Firebase
cloud_firestore: ^6.1.0 # la base de données Firestore
Les versions indiquées sont celles de novembre 2026 : vérifiez les plus récentes sur pub.dev et gardez firebase_core et cloud_firestore de la même génération.
Les SDK Firebase récents exigent minSdk 23 au minimum. Si la compilation affiche une erreur minSdkVersion, remplacez dans
android/app/build.gradle.kts la ligne minSdk = flutter.minSdkVersion par minSdk = 23. 3.6 Créer la base Firestore et ses règles de test
- Console Firebase › Créer › Firestore Database › Créer une base de données.
- Choisissez un emplacement proche (ex.
europe-west1) : il ne pourra plus être changé. - Démarrez en mode production, puis ouvrez l'onglet Règles et collez ces règles de TP (accès ouvert à la collection
stagiairesjusqu'à une date limite) :
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /stagiaires/{id} {
allow read, write: if request.time < timestamp.date(2027, 1, 31);
}
}
}
Cliquez sur Publier. Ces règles conviennent pour apprendre, jamais pour une application publiée : la leçon sur Firebase Authentication montre comment les sécuriser (request.auth != null, rôles…).
🛠 Activité 1 — Configurer FormaApp à la main
Mission : Créez le projet Flutter forma_app, le projet Firebase formaapp-votrenom, puis réalisez les étapes 1 à 5 et la création de la base, sans aucun outil en ligne de commande.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Quel
applicationIdvotre projet a-t-il reçu ? Dans quel fichier l'avez-vous trouvé ? Que se passe-t-il si le nom saisi dans Firebase est différent ? - Ouvrez votre
google-services.json: relevez les valeursproject_id,package_nameetmobilesdk_app_id. À quoi sert chacune, selon vous ? - Vos fichiers Gradle sont-ils en
.ktsou en Groovy ? Quelle méthode des étapes 3 et 4 avez-vous donc utilisée ? - Retirez volontairement la ligne de l'étape 4, puis compilez. Le message d'erreur apparaît-il à la compilation ou au lancement de l'application ? Remettez la ligne.
- Pourquoi la règle de test contient-elle une date limite plutôt que
if true? Adaptez-la à votre collectionformations.
Je vérifie / je rends :
- ☐ Capture de la page « Paramètres du projet » montrant votre application Android.
- ☐ Capture de l'onglet « Règles » publié.
- ☐ L'application compile et démarre sur l'émulateur.
Piste (sans solution) : si la compilation échoue, lisez la première ligne « What went wrong » du terminal : elle nomme presque toujours le fichier fautif.
4. Liaison : initialiser Firebase dans l'application
4.1 Étape 6 : écrire firebase_options.dart à la main
Ce fichier contient les identifiants de votre projet Firebase, sous forme de code Dart. Créez lib/firebase_options.dart et recopiez les valeurs de votre google-services.json:
// lib/firebase_options.dart (écrit à la main, Android uniquement)
import 'package:firebase_core/firebase_core.dart';
/*
Où trouver les valeurs dans android/app/google-services.json :
apiKey <- client[0].api_key[0].current_key
appId <- client[0].client_info.mobilesdk_app_id
messagingSenderId <- project_info.project_number
projectId <- project_info.project_id
storageBucket <- project_info.storage_bucket
*/
class DefaultFirebaseOptions {
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSy...votre_cle',
appId: '1:123456789012:android:abc123def456',
messagingSenderId: '123456789012',
projectId: 'gestion-stagiaires-xxxx',
storageBucket: 'gestion-stagiaires-xxxx.firebasestorage.app',
);
static FirebaseOptions get currentPlatform => android;
}
La clé
apiKey n'est pas un secret : elle identifie votre projet et se retrouve de toute façon dans l'application installée. Ce sont les règles de sécurité Firestore qui protègent vos données.4.2 Étape 7 : initialiser Firebase dans main.dart
Firebase doit être initialisé avant runApp(). Il y a deux façons de le faire :
Méthode 1 Sans options : Firebase lit google-services.json (Android seulement)
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); // obligatoire avant tout await
await Firebase.initializeApp(); // utilise google-services.json (étapes 2 à 4)
runApp(const MyApp());
}
Cette méthode n'a pas besoin de firebase_options.dart: sur Android, le plugin Google Services a transformé google-services.json en ressources que Firebase sait lire. Elle ne fonctionne pas pour le Web.
Méthode 2 Avec options : Firebase utilise firebase_options.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, // valeurs de l'étape 6
);
runApp(const MyApp());
}
Cette méthode est indispensable pour le Web et pour plusieurs plateformes : il suffit d'ajouter dans firebase_options.dart une configuration web, ios… et de choisir la bonne dans currentPlatform.
| Méthode 1 : sans options | Méthode 2 : avec firebase_options.dart | |
|---|---|---|
| Fichier lu | google-services.json |
lib/firebase_options.dart |
| Plugin Gradle (étapes 3-4) | Obligatoire | Conseillé sur Android |
| Plateformes | Android (et iOS avec son propre fichier) | Toutes, Web compris |
| Quand l'utiliser ? | TP rapide, Android uniquement | Projet réel, multi-plateforme |
• Oublier
async sur main ou le mot await: erreur [core/no-app] No Firebase App '[DEFAULT]' has been created. • Oublier
WidgetsFlutterBinding.ensureInitialized(): erreur « Binding has not yet been initialized ».4.3 Tester la liaison
Ajoutez temporairement ce bouton dans votre page d'accueil, cliquez dessus, puis ouvrez Firestore Database › Données dans la console : une collection stagiaires doit apparaître.
ElevatedButton(
onPressed: () async {
await FirebaseFirestore.instance
.collection('stagiaires')
.add({'nom': 'Test de connexion', 'creeLe': FieldValue.serverTimestamp()});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Document écrit dans Firestore ✅')),
);
},
child: const Text('Tester Firestore'),
),
(Importez package:cloud_firestore/cloud_firestore.dart. )
🛠 Activité 2 — Relier FormaApp à Firebase
Mission : Écrivez votre firebase_options.dart, testez les deux méthodes d'initialisation, puis écrivez une première formation dans Firestore.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Testez la méthode 1, puis la méthode 2. Les deux fonctionnent-elles sur votre émulateur Android ? Laquelle garderez-vous pour FormaApp, et pourquoi ?
- Lancez l'application dans Chrome (Web) avec la méthode 1. Quel message obtenez-vous ? Qu'expliquez-vous ?
- Supprimez
WidgetsFlutterBinding.ensureInitialized();puis relancez. Notez le message d'erreur exact dans votre fiche de dépannage. - Avec le bouton de test, écrivez une formation. Quel identifiant Firestore a-t-il attribué au document ? Qui l'a choisi ?
- Cliquez trois fois sur le bouton : combien de documents obtenez-vous ? Pourquoi ?
Je vérifie / je rends :
- ☐ Capture de la console Firestore montrant votre premier document.
- ☐ La fiche de dépannage commencée (erreur → cause → solution).
5. Préparer le CRUD : la référence et le modèle
Toutes les opérations partent d'une référence de collection. Pour éviter de manipuler des Map partout, on crée aussi une petite classe modèle qui sait se lire depuis Firestore et s'y écrire.
// lib/models/stagiaire.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class Stagiaire {
final String id; // identifiant du document Firestore
final String nom;
final String groupe;
final double moyenne;
const Stagiaire({
required this.id,
required this.nom,
required this.groupe,
required this.moyenne,
});
/// Construit un Stagiaire à partir d'un document lu dans Firestore.
factory Stagiaire.fromDoc(DocumentSnapshot<Map<String, dynamic>> doc) {
final data = doc.data()!;
return Stagiaire(
id: doc.id,
nom: data['nom'] as String? ?? '',
groupe: data['groupe'] as String? ?? '',
moyenne: (data['moyenne'] as num? ?? 0).toDouble(),
);
}
/// Données à écrire dans Firestore (l'id n'est pas un champ : c'est le nom du document).
Map<String, dynamic> toMap() => {
'nom': nom,
'nomMin': nom.toLowerCase(), // copie en minuscules pour la recherche (11.3)
'groupe': groupe,
'moyenne': moyenne,
};
}
// lib/services/stagiaire_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/stagiaire.dart';
class StagiaireService {
final CollectionReference<Map<String, dynamic>> stagiaires =
FirebaseFirestore.instance.collection('stagiaires');
// Les méthodes des parties 6 à 11 se placent ici.
}
(data['moyenne'] as num? ?? 0).toDouble(): Firestore renvoie 15 (un int) si vous avez écrit 15, et 15.5 (un double) si vous avez écrit 15.5. Passer par num évite l'erreur « type 'int' is not a subtype of type 'double' ».6. Ajouter un document (Create)
6.1 Méthode 1 : add(), identifiant généré par Firestore
Future<String> ajouter(Stagiaire s) async {
final ref = await stagiaires.add({
...s.toMap(),
'creeLe': FieldValue.serverTimestamp(), // date fixée par le serveur
});
return ref.id; // ex. "Xk3aP9q2LmZ8"
}
À utiliser quand l'identifiant n'a pas de sens métier (un message, une commande, un commentaire).
6.2 Méthode 2 : doc(id).set(), identifiant choisi
Future<void> ajouterAvecId(String matricule, Stagiaire s) async {
await stagiaires.doc(matricule).set({
...s.toMap(),
'creeLe': FieldValue.serverTimestamp(),
});
}
À utiliser quand un identifiant naturel existe : matricule, numéro de CIN, UID d'un utilisateur connecté.
Si un document porte déjà cet identifiant,
set() le remplace entièrement, sans erreur. Pour refuser les doublons, lisez d'abord le document (partie 7) et vérifiez doc.exists. 7. Lire un document par son identifiant

Figure 3 — Les deux méthodes de lecture, valables pour les parties 7, 8 et 11.
7.1 Méthode 1 : get(), lecture une seule fois (Future)
// Dans StagiaireService
Future<Stagiaire?> lireParId(String id) async {
final doc = await stagiaires.doc(id).get();
if (!doc.exists) return null; // identifiant inconnu
return Stagiaire.fromDoc(doc);
}
Dans l'interface, avec un FutureBuilder:
FutureBuilder<Stagiaire?>(
future: service.lireParId(id),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) return Text('Erreur : ${snapshot.error}');
final s = snapshot.data;
if (s == null) return const Text('Stagiaire introuvable');
return ListTile(
title: Text(s.nom),
subtitle: Text('${s.groupe} – ${s.moyenne}/20'),
);
},
)
7.2 Méthode 2 : snapshots(), lecture en temps réel (Stream)
// Dans StagiaireService
Stream<Stagiaire?> suivreParId(String id) {
return stagiaires.doc(id).snapshots().map(
(doc) => doc.exists ? Stagiaire.fromDoc(doc) : null,
);
}
StreamBuilder<Stagiaire?>(
stream: service.suivreParId(id),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) return Text('Erreur : ${snapshot.error}');
final s = snapshot.data;
if (s == null) return const Text('Stagiaire introuvable');
return ListTile(
title: Text(s.nom),
subtitle: Text('${s.groupe} – ${s.moyenne}/20'),
);
},
)
Modifiez la moyenne dans la console Firebase pendant que l'écran est ouvert : avec la méthode 2, la nouvelle valeur s'affiche aussitôt ; avec la méthode 1, il faut recharger l'écran.
8. Lire tous les documents d'une collection
8.1 Méthode 1 : get(), une seule fois (Future)
// Dans StagiaireService
Future<List<Stagiaire>> lireTous() async {
final snap = await stagiaires.orderBy('nom').get();
return snap.docs.map(Stagiaire.fromDoc).toList();
}
FutureBuilder<List<Stagiaire>>(
future: service.lireTous(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) return Text('Erreur : ${snapshot.error}');
final liste = snapshot.data!;
if (liste.isEmpty) return const Center(child: Text('Aucun stagiaire'));
return ListView.builder(
itemCount: liste.length,
itemBuilder: (context, i) => ListTile(
title: Text(liste[i].nom),
subtitle: Text('${liste[i].groupe} – ${liste[i].moyenne}/20'),
),
);
},
)
8.2 Méthode 2 : snapshots(), en temps réel (Stream)
// Dans StagiaireService
Stream<List<Stagiaire>> suivreTous() {
return stagiaires.orderBy('nom').snapshots().map(
(snap) => snap.docs.map(Stagiaire.fromDoc).toList(),
);
}
StreamBuilder<List<Stagiaire>>(
stream: service.suivreTous(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) return Text('Erreur : ${snapshot.error}');
final liste = snapshot.data!;
if (liste.isEmpty) return const Center(child: Text('Aucun stagiaire'));
return ListView.builder(
itemCount: liste.length,
itemBuilder: (context, i) => ListTile(
title: Text(liste[i].nom),
subtitle: Text('${liste[i].groupe} – ${liste[i].moyenne}/20'),
),
);
},
)
Quelle méthode choisir ?
get() pour un rapport, un export, un calcul (moyenne de la promotion) ou un écran qui ne change pas. snapshots() pour une liste ou un tableau de bord qui doit rester à jour sans bouton « Actualiser ». Avec snapshots(), après un ajout ou une suppression, inutile de recharger la liste : elle se met à jour toute seule.Ne créez pas le
Future ou le Stream directement dans build() d'un StatefulWidget qui se reconstruit souvent : chaque reconstruction relancerait une lecture. Créez-le une fois dans initState() et rangez-le dans une variable.9. Modifier un document (Update)
9.1 Méthode 1 : update(), modifier seulement certains champs
Future<void> modifierMoyenne(String id, double moyenne) async {
await stagiaires.doc(id).update({'moyenne': moyenne});
}
// Si le nom change, mettre aussi à jour sa copie en minuscules
Future<void> modifierNom(String id, String nom) async {
await stagiaires.doc(id).update({'nom': nom, 'nomMin': nom.toLowerCase()});
}
Les autres champs ne sont pas touchés. Si le document n'existe pas, update() échoue avec l'erreur not-found.
9.2 Méthode 2 : set() avec SetOptions(merge: true), modifier ou créer
Future<void> modifierOuCreer(String id, Map<String, dynamic> champs) async {
await stagiaires.doc(id).set(champs, SetOptions(merge: true));
}
// exemple : modifierOuCreer('S042', {'moyenne': 16.5});
Les champs envoyés sont fusionnés avec ceux du document ; si le document n'existe pas, il est créé.
Sans
SetOptions(merge: true), set({'moyenne': 16.5}) remplace tout le document : le nom et le groupe sont perdus.10. Supprimer (Delete)
10.1 Méthode 1 : supprimer un document par son identifiant
Future<void> supprimer(String id) async {
await stagiaires.doc(id).delete();
}
Supprimer un identifiant qui n'existe pas ne provoque pas d'erreur. Demandez toujours une confirmation à l'utilisateur avant d'appeler cette méthode (showDialog).
10.2 Méthode 2 : supprimer plusieurs documents selon une condition (batch)
// Supprimer tous les stagiaires d'un groupe, en une seule opération
Future<int> supprimerGroupe(String groupe) async {
final snap = await stagiaires.where('groupe', isEqualTo: groupe).get();
final batch = FirebaseFirestore.instance.batch();
for (final doc in snap.docs) {
batch.delete(doc.reference);
}
await batch.commit(); // tout réussit, ou rien n'est supprimé
return snap.docs.length;
}
Firestore n'a pas de « DELETE … WHERE » : on lit les documents concernés, puis on les supprime ensemble dans un batch (lot), limité à 500 opérations.
Supprimer un document ne supprime pas ses sous-collections éventuelles (par exemple
stagiaires/S042/absences) : elles doivent être supprimées séparément.🛠 Activité 3 — Le CRUD de FormaApp
Mission : Créez le modèle Formation, le service FormationService et les écrans : liste des formations, fiche d'une formation, formulaire d'ajout et de modification, suppression avec confirmation.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Quels champs mettez-vous dans le modèle
Formation? De quel type est chacun ? Pourquoi l'identifiant n'est-il pas danstoMap()? - Pour l'ajout, choisissez-vous
add()ouset()? Le code de formation officiel (ex. « DSI-BTS-01 ») pourrait-il servir d'identifiant ? Avantages et risques ? - Affichez la liste avec la méthode 1, puis avec la méthode 2. Ajoutez une formation depuis la console Firebase : que se passe-t-il dans chaque cas ?
- Votre écran de modification doit changer seulement le nombre de places. Quelle méthode de la partie 9 utilisez-vous ? Que se passerait-il avec un simple
set()? - Ajoutez un bouton « Supprimer toutes les formations de niveau CAP ». Quelle méthode utilisez-vous ? Que se passe-t-il s'il n'y en a aucune ?
- Où placez-vous le
Futureou leStreampour éviter de relancer la lecture à chaquesetState()?
Je vérifie / je rends :
- ☐ Ajout, liste, fiche, modification et suppression fonctionnels.
- ☐ Capture de la collection
formationsdans la console avec au moins 6 documents.
Piste (sans solution) : écrivez d'abord le service et testez chaque méthode avec un simple bouton et un print(), avant de construire les écrans.
11. Recherche
Une recherche est une requête: on ajoute des conditions (where), un tri (orderBy) et éventuellement une limite (limit), puis on la lit avec l'une des deux méthodes vues aux parties 7 et 8 : get() (une fois) ou snapshots() (temps réel).
11.1 Recherche par égalité (isEqualTo)
Exemple : les stagiaires d'un groupe.
Méthode 1 une fois, get()
Future<List<Stagiaire>> parGroupe(String groupe) async {
final snap = await stagiaires.where('groupe', isEqualTo: groupe).get();
return snap.docs.map(Stagiaire.fromDoc).toList();
}
Méthode 2 en temps réel, snapshots()
Stream<List<Stagiaire>> suivreParGroupe(String groupe) {
return stagiaires.where('groupe', isEqualTo: groupe).snapshots().map(
(snap) => snap.docs.map(Stagiaire.fromDoc).toList(),
);
}
11.2 Recherche par comparaison, avec tri et limite
Exemple : les 10 meilleurs stagiaires ayant au moins 10 de moyenne.
Méthode 1 une fois, get()
Future<List<Stagiaire>> admis() async {
final snap = await stagiaires
.where('moyenne', isGreaterThanOrEqualTo: 10)
.orderBy('moyenne', descending: true)
.limit(10)
.get();
return snap.docs.map(Stagiaire.fromDoc).toList();
}
Méthode 2 en temps réel, snapshots()
Stream<List<Stagiaire>> suivreAdmis() {
return stagiaires
.where('moyenne', isGreaterThanOrEqualTo: 10)
.orderBy('moyenne', descending: true)
.limit(10)
.snapshots()
.map((snap) => snap.docs.map(Stagiaire.fromDoc).toList());
}
11.3 Recherche par nom : « commence par »
Firestore n'a ni LIKE ni recherche plein texte. On cherche les noms qui commencent par le texte saisi, dans le champ nomMin (le nom en minuscules, écrit par toMap()). Le caractère '\uf8ff', placé très loin dans l'ordre alphabétique, sert de borne de fin :
Méthode 1 une fois, get() (bouton « Rechercher »)
Future<List<Stagiaire>> rechercherNom(String saisie) async {
final t = saisie.trim().toLowerCase();
final snap = await stagiaires
.where('nomMin', isGreaterThanOrEqualTo: t)
.where('nomMin', isLessThan: '$t\uf8ff')
.get();
return snap.docs.map(Stagiaire.fromDoc).toList();
}
Méthode 2 en temps réel, snapshots() (résultats pendant la saisie)
Stream<List<Stagiaire>> suivreRechercheNom(String saisie) {
final t = saisie.trim().toLowerCase();
return stagiaires
.where('nomMin', isGreaterThanOrEqualTo: t)
.where('nomMin', isLessThan: '$t\uf8ff')
.snapshots()
.map((snap) => snap.docs.map(Stagiaire.fromDoc).toList());
}
// Dans l'écran : un champ de recherche qui relance le flux à chaque frappe
String saisie = '';
TextField(
decoration: const InputDecoration(
labelText: 'Rechercher un stagiaire',
prefixIcon: Icon(Icons.search),
),
onChanged: (texte) => setState(() => saisie = texte),
),
Expanded(
child: StreamBuilder<List<Stagiaire>>(
stream: service.suivreRechercheNom(saisie),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Center(child: CircularProgressIndicator());
final liste = snapshot.data!;
if (liste.isEmpty) return const Center(child: Text('Aucun résultat'));
return ListView(
children: [for (final s in liste) ListTile(title: Text(s.nom))],
);
},
),
),
Cette recherche ne trouve que le début du texte : « ben » ne trouve pas « Amira Ben Ali ». Pour chercher dans tout le nom, on stocke un tableau de mots (
'motsCles': ['amira', 'ben', 'ali']) et on utilise where('motsCles', arrayContains: 'ben'). Pour une vraie recherche plein texte, il faut un service externe (Algolia, Typesense…).11.4 Recherche avec plusieurs conditions
Exemple : les admis d'un groupe, du meilleur au moins bon.
Méthode 1 une fois, get()
Future<List<Stagiaire>> admisDuGroupe(String groupe) async {
final snap = await stagiaires
.where('groupe', isEqualTo: groupe)
.where('moyenne', isGreaterThanOrEqualTo: 10)
.orderBy('moyenne', descending: true)
.get();
return snap.docs.map(Stagiaire.fromDoc).toList();
}
Méthode 2 en temps réel, snapshots()
Stream<List<Stagiaire>> suivreAdmisDuGroupe(String groupe) {
return stagiaires
.where('groupe', isEqualTo: groupe)
.where('moyenne', isGreaterThanOrEqualTo: 10)
.orderBy('moyenne', descending: true)
.snapshots()
.map((snap) => snap.docs.map(Stagiaire.fromDoc).toList());
}
Une requête qui combine une égalité sur un champ (
groupe) et une comparaison ou un tri sur un autre (moyenne) exige un index composite. À la première exécution, la console de débogage affiche une erreur failed-precondition avec un lien: ouvrez-le, cliquez sur Créer l'index, attendez quelques minutes que l'index soit prêt (onglet Index), puis relancez.🛠 Activité 4 — Rechercher dans FormaApp
Mission : Ajoutez à FormaApp un écran de recherche : par niveau, par nombre de places disponibles, par début d'intitulé, et une combinaison de critères.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Écrivez la requête « toutes les formations de niveau BTS ». Utilisez-la avec
get()dans un bouton, puis avecsnapshots()dans une liste. Quelle différence observez-vous quand vous modifiez un niveau dans la console ? - Écrivez la requête « formations ayant au moins 15 places, de la plus longue à la plus courte ». Fonctionne-t-elle du premier coup ? Si Firestore demande un index, pourquoi ?
- Votre recherche par intitulé trouve-t-elle « Développement » quand on tape « dév » ? Et « développement » quand on tape « Dév » ? Quel champ devez-vous ajouter à vos documents, et que faire pour les documents déjà créés ?
- Tapez « web » : la formation « Développement web et mobile » est-elle trouvée ? Expliquez, puis proposez une solution avec
arrayContains. - Combinez niveau + places disponibles. Combien d'index composites votre projet compte-t-il maintenant (onglet Index de la console) ?
Je vérifie / je rends :
- ☐ Écran de recherche fonctionnel avec au moins 4 critères.
- ☐ Capture de l'onglet Index de la console.
- ☐ Fiche de dépannage complétée (au moins 5 erreurs).
12. Problèmes courants et solutions
| Problème | Cause probable | Solution |
|---|---|---|
File google-services.json is missing |
Fichier absent ou mal placé | Le placer dans android/app/ (étape 2) |
No matching client found for package name |
Nom de package différent dans Firebase et dans build.gradle.kts |
Comparer applicationId et package_name (étape 1) |
Plugin [id: 'com.google.gms.google-services'] was not found |
Plugin non déclaré | Étape 3 (méthode correspondant à vos fichiers) |
| Erreur minSdkVersion | minSdk trop bas | minSdk = 23 (étape 5) |
[core/no-app] No Firebase App '[DEFAULT]' has been created |
initializeApp() absent ou sans await |
Étape 7 |
[cloud_firestore/permission-denied] |
Règles qui refusent, ou date limite dépassée | Vérifier l'onglet Règles (partie 3.6) |
[cloud_firestore/not-found] sur un update() |
Le document n'existe pas | Vérifier l'identifiant, ou utiliser set(…, merge: true) |
[cloud_firestore/failed-precondition] The query requires an index |
Index composite manquant | Ouvrir le lien affiché (partie 11.4) |
type 'int' is not a subtype of type 'double' |
Nombre entier écrit dans un champ lu comme double |
Lire avec (… as num).toDouble() (partie 5) |
| La liste ne se met pas à jour après un ajout | Lecture avec get() |
Utiliser snapshots() ou relire après l'ajout |
13. Récapitulatif
Configuration manuelle : ① applicationId dans android/app/build.gradle.kts → ② google-services.json dans android/app/ → ③ plugin déclaré dans settings.gradle.kts (ou buildscript) → ④ plugin appliqué dans app/build.gradle.kts → ⑤ packages dans pubspec.yaml et minSdk 23 → ⑥ firebase_options.dart → ⑦ Firebase.initializeApp() dans main.dart.
| Partie | Opération | Méthode 1 | Méthode 2 |
|---|---|---|---|
| 6 | Ajouter | add(): identifiant automatique |
doc(id).set(): identifiant choisi |
| 7 | Lire par identifiant | doc(id).get(): une fois |
doc(id).snapshots(): temps réel |
| 8 | Lire tout | get(): une fois |
snapshots(): temps réel |
| 9 | Modifier | update(): document existant |
set(…, SetOptions(merge: true)): modifie ou crée |
| 10 | Supprimer | doc(id).delete(): un document |
batch.delete(): selon une condition |
| 11 | Rechercher | where(…).get(): une fois |
where(…).snapshots(): temps réel |
Ressource PDF disponible
Ce cours contient une version PDF téléchargeable. Le lien sera disponible prochainement.