Développement Mobile

Push notifications en Flutter avec Firebase Cloud Messaging pas à pas

15 min de lecture

Les push notifications sont la passerelle la plus directe entre une application mobile et l’utilisateur après fermeture. Bien faites, elles ramènent un client vers une commande en cours, alertent d’un évènement de stock ou confirment une livraison. Mal faites, elles agacent et se font désactiver en deux clics. Ce tutoriel intègre Firebase Cloud Messaging (FCM) dans une application Flutter de bout en bout : création du projet Firebase, configuration Android et iOS, gestion des permissions Android 13+, récupération du token, réception en foreground et background, et test depuis la console Firebase.

📖 Guide principal : Flutter 3 et Dart 3 pour le développement mobile cross-platform — pour le contexte général et les pièges récurrents, lisez d’abord cette page.

Prérequis

  • Flutter SDK 3.41+ (tutoriel d’installation).
  • Compte Google pour créer un projet Firebase.
  • Application Flutter de base (idéalement l’app Riverpod du tutoriel précédent).
  • Pour iOS : Mac avec Xcode 15+, compte Apple Developer (99 $/an) pour générer un certificat APNs en production.
  • Node.js installé pour la FlutterFire CLI.
  • Niveau : intermédiaire. Temps estimé : 90 minutes.

Étape 1 — Créer un projet Firebase et installer la CLI

Tout commence côté serveur : un projet Firebase est l’unité d’organisation qui regroupe vos applications (Android, iOS, web) sous une même configuration. La console Firebase fournit l’interface pour le créer en quelques clics.

Rendez-vous sur console.firebase.google.com et cliquez sur Ajouter un projet. Donnez-lui un nom, désactivez Google Analytics si vous n’en avez pas besoin (vous pourrez l’activer plus tard), puis validez. Le provisionnement prend une trentaine de secondes.

Côté machine de développement, vous avez besoin de deux outils en ligne de commande : firebase-tools (la CLI officielle Firebase pour la gestion du projet) et flutterfire_cli (la passerelle Dart qui génère automatiquement firebase_options.dart). Installez-les :

npm install -g firebase-tools
firebase login
dart pub global activate flutterfire_cli

La commande firebase login ouvre votre navigateur pour l’authentification Google. Une fois connecté, vérifiez avec firebase projects:list que votre nouveau projet apparaît. La CLI FlutterFire dépend de la CLI Firebase pour récupérer les credentials — gardez les deux installées.

Étape 2 — Ajouter Firebase au projet Flutter

La FlutterFire CLI automatise tout ce qui était manuel à une époque : déclaration des plateformes, génération de firebase_options.dart, configuration du google-services.json (Android) et du GoogleService-Info.plist (iOS).

Depuis la racine de votre projet Flutter :

flutterfire configure

La CLI vous demande de choisir le projet Firebase, puis les plateformes à activer (cochez au moins Android et iOS), et écrit un fichier lib/firebase_options.dart. Ce fichier contient les credentials publics nécessaires à l’initialisation côté client — il peut être commité dans Git sans risque.

Ajoutez ensuite les paquets Firebase pertinents :

flutter pub add firebase_core
flutter pub add firebase_messaging
flutter pub add flutter_local_notifications

Au moment de cette rédaction, les versions résolues sont firebase_core: ^4.x.x, firebase_messaging: ^16.0.0 (vérifiez la dernière sur pub.dev) et flutter_local_notifications: ^21.0.0. Le paquet flutter_local_notifications sert à afficher la notification dans la barre Android quand l’app est en foreground (FCM ne le fait pas tout seul dans ce cas).

Étape 3 — Initialiser Firebase au démarrage

Avant tout appel à FirebaseMessaging, vous devez initialiser le SDK Firebase. C’est une étape rapide mais non-skippable : sans elle, le premier FirebaseMessaging.instance lèvera une exception.

Ouvrez lib/main.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,
  );
  runApp(const MyApp());
}

L’appel DefaultFirebaseOptions.currentPlatform est généré par flutterfire configure et résout automatiquement les bons credentials selon l’OS du build. Vérifiez que l’app compile encore avec flutter run — si firebase_options.dart est manquant, relancez flutterfire configure.

Étape 4 — Demander la permission de notification

Depuis Android 13 (API 33), la permission POST_NOTIFICATIONS doit être demandée explicitement à l’utilisateur. Sans cette demande, vos notifications sont silencieusement bloquées par le système — l’erreur la plus pénible à diagnostiquer parce que le SDK Firebase ne lève pas d’exception, il accepte juste de ne rien afficher.

Sur iOS, la permission est demandée par le même appel requestPermission() du SDK, qui affiche la boîte de dialogue système APNs.

import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> setupMessaging() async {
  final messaging = FirebaseMessaging.instance;

  final settings = await messaging.requestPermission(
    alert: true,
    badge: true,
    sound: true,
    provisional: false,
  );

  switch (settings.authorizationStatus) {
    case AuthorizationStatus.authorized:
      print('Notifications autorisées');
      break;
    case AuthorizationStatus.provisional:
      print('Autorisation provisoire (iOS)');
      break;
    case AuthorizationStatus.denied:
      print('Notifications refusées');
      break;
    case AuthorizationStatus.notDetermined:
      print('Permission non encore demandée');
      break;
  }
}

Le bon moment pour appeler setupMessaging() est juste après le login utilisateur ou à un point où la valeur des notifications est claire pour l’utilisateur — pas au tout premier démarrage avant qu’il sache à quoi sert votre app. Une demande prématurée se solde par un refus dans la majorité des cas, et une fois refusé, c’est définitif jusqu’au reset manuel par l’utilisateur dans les paramètres système.

Étape 5 — Récupérer et stocker le token FCM

Chaque installation de votre app reçoit un token FCM unique, qui sert d’adresse de destination pour les notifications envoyées par votre backend. Ce token est généré à la première initialisation et peut être révoqué/régénéré par le système (changement de SIM, réinstallation, restauration de backup).

final token = await FirebaseMessaging.instance.getToken();
print('FCM Token : $token');

// Côté backend : associer ce token à l'utilisateur connecté
await api.post('/devices', body: {'fcm_token': token});

// Écouter les régénérations
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
  await api.post('/devices', body: {'fcm_token': newToken});
});

Deux discipline à observer. D’abord, envoyez systématiquement le token à votre backend après le login — sans cela, vous ne pouvez cibler personne. Ensuite, écoutez onTokenRefresh et renvoyez la nouvelle valeur ; ignorer cet event finit par produire des tokens fantômes côté backend qui ne livrent plus rien.

Étape 6 — Gérer la réception en foreground, background et terminé

FCM distingue trois cas de réception selon l’état de votre application au moment où la notification arrive. Vous devez câbler les trois handlers ou des messages passeront silencieusement.

Foreground (app ouverte et visible) : Android n’affiche rien automatiquement, à vous de pousser une notification locale ou de mettre à jour l’UI directement.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Foreground : ${message.notification?.title}');
  // Afficher une notification locale ou mettre à jour l'UI
});

Background (app en arrière-plan, pas tuée) : le système affiche la notification et le SDK peut exécuter du code Dart en arrière-plan via un handler dédié. Ce handler doit être une fonction top-level, pas une méthode de classe ni une closure.

@pragma('vm:entry-point')
Future<void> _backgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  print('Background : ${message.messageId}');
  // Persistance locale, logging, mise à jour de cache
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
  runApp(const MyApp());
}

L’annotation @pragma('vm:entry-point') garantit que le code n’est pas éliminé par le tree-shaking lors du build release. C’est obligatoire en AOT.

Terminé (app fermée, lancée par l’utilisateur depuis la notification) : la notification d’origine est disponible via getInitialMessage() au démarrage. C’est ce qui vous permet de router l’utilisateur vers l’écran lié.

final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
  print('Lancée depuis notification : ${initial.data}');
  // Naviguer vers l'écran approprié selon initial.data
}

Étape 7 — Configurer Android pour les notifications

Côté Android, deux configurations sont à vérifier. D’abord, le channel de notification — depuis Android 8 (API 26), toutes les notifications doivent appartenir à un channel. Sans channel déclaré, vos notifications n’apparaissent pas.

Avec flutter_local_notifications, déclarez le channel au démarrage :

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

const channel = AndroidNotificationChannel(
  'high_importance_channel',
  'Notifications importantes',
  description: 'Notifications critiques de l\'application',
  importance: Importance.high,
);

final localNotifs = FlutterLocalNotificationsPlugin();

await localNotifs
    .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin>()
    ?.createNotificationChannel(channel);

Ensuite, le handler foreground doit appeler manuellement localNotifs.show() pour afficher la notification dans la barre — FCM ne le fait pas tout seul dans cet état :

FirebaseMessaging.onMessage.listen((message) {
  final notif = message.notification;
  final android = message.notification?.android;
  if (notif != null && android != null) {
    localNotifs.show(
      id: notif.hashCode,
      title: notif.title,
      body: notif.body,
      notificationDetails: NotificationDetails(
        android: AndroidNotificationDetails(
          channel.id,
          channel.name,
          icon: android.smallIcon,
        ),
      ),
    );
  }
});

Le hash du titre + corps comme id évite de reproduire la même notification dix fois si l’utilisateur reste sur l’écran d’accueil.

Étape 8 — Configurer iOS et APNs

Sur iOS, FCM s’interpose entre votre serveur et le réseau APNs d’Apple, mais APNs reste la passerelle système — vous devez donc activer la capability Push Notifications dans Xcode et fournir un certificat ou une clé d’authentification APNs à Firebase.

Étapes :

  1. Ouvrez ios/Runner.xcworkspace dans Xcode (jamais Runner.xcodeproj seul).
  2. Sélectionnez la target Runner, onglet Signing & Capabilities, cliquez + Capability, ajoutez Push Notifications et Background Modes.
  3. Dans Background Modes, cochez Remote notifications.
  4. Sur developer.apple.com, créez une APNs Authentication Key (.p8). Téléchargez-la.
  5. Dans la console Firebase → Paramètres du projet → Cloud Messaging → iOS app configuration, uploadez la clé .p8 avec le Key ID et le Team ID.

Sans la clé .p8 chargée dans Firebase, le SDK est silencieux sur iOS — aucune erreur, aucune notification reçue. C’est l’un des pièges les plus pénibles de la première intégration.

Étape 9 — Tester depuis la console Firebase

Le moyen le plus rapide de valider l’intégration de bout en bout est la fonction Test de la console Firebase. Allez dans EngagementMessagingNouvelle campagneNotifications. Renseignez titre et corps, choisissez Envoyer un message de test, collez le token FCM affiché par votre app, validez.

La notification doit apparaître sur le téléphone en quelques secondes. Si rien n’arrive, vérifiez dans l’ordre : permission accordée (sur Android), channel créé, token valide (en générer un neuf au besoin), pour iOS le certificat APNs uploadé. Les Logs de la console Firebase indiquent si le message a été délivré à APNs/FCM ou non.

Anatomie d’une notification FCM, du backend au téléphone

Comprendre le chemin parcouru par une notification éclaire les diagnostics et oriente les décisions de design. Le trajet est court mais traverse plusieurs systèmes.

Votre backend appelle l’API HTTPS de FCM avec un JSON décrivant la cible (token, topic, ou condition logique) et le contenu. FCM reçoit, persiste si nécessaire (un téléphone éteint reçoit jusqu’à 100 messages non-collapsibles mis en attente), puis route. Sur Android, FCM utilise Google Play Services, présent par défaut sur la quasi-totalité des appareils livrés avec services Google (la plupart des marchés hors Chine continentale, où Huawei et ses alternatives dominent). Sur iOS, FCM transmet à APNs, qui notifie le téléphone via le canal persistant maintenu par le système Apple.

Le téléphone reçoit le message, le SDK Firebase de votre application est réveillé, et selon l’état (foreground/background/terminé) déclenche le bon callback Dart. C’est à cette étape que les choses peuvent silencieusement échouer : permission refusée, channel manquant, capability iOS désactivée. La console Firebase Logs vous dit si le message a été reçu côté serveur ou non, mais pas si l’utilisateur l’a vu — le téléphone n’envoie pas d’accusé de réception par défaut.

Pour mesurer la délivrabilité réelle, activez Google Analytics dans votre projet Firebase : il agrège messages envoyés, impressions (vues) et ouvertures par campagne, ce qui donne le seul indicateur utile pour ajuster votre stratégie.

Topics ou tokens : quand utiliser quoi

FCM offre deux modes de ciblage. Le token individuel envoie à un appareil précis — c’est ce qu’il faut pour les notifications transactionnelles : confirmation de commande, alerte de connexion, code de validation. Le topic envoie à tous les appareils abonnés à un identifiant logique — typique pour des broadcasts : nouvelles fonctionnalités, alertes générales, mises à jour de catalogue.

L’abonnement à un topic se fait côté client :

await FirebaseMessaging.instance.subscribeToTopic('updates_fr');
await FirebaseMessaging.instance.unsubscribeFromTopic('updates_fr');

Le serveur n’a alors plus besoin de connaître la liste de tokens — il envoie au topic, FCM dispatche. C’est plus simple à coder, mais perd la traçabilité : impossible de savoir précisément qui a reçu quoi. Pour des campagnes de masse non-personnalisées, le topic est imbattable ; pour de la communication 1:1, restez sur tokens.

Bonnes pratiques UX et anti-spam

Le piège classique avec les notifications, c’est qu’elles sont techniquement faciles à envoyer mais coûteuses à mal calibrer. Une part importante d’utilisateurs désactive les notifications d’une app jugée envahissante dans les premières semaines suivant l’installation — les chiffres varient entre éditeurs et études, mais la tendance est claire. Quelques règles tiennent debout.

D’abord, ne demandez jamais la permission au tout premier démarrage. Attendez le moment où l’utilisateur a compris la valeur de l’app — typiquement après le premier usage réussi, après la création du compte, ou juste avant un évènement où la notification est utile (suivi de livraison après achat). Les statistiques publiques sur les taux d’opt-in donnent un ordre de grandeur fiable : autour de 50 % d’acceptation sur iOS et 85 % sur Android (qui demande la permission depuis Android 13). Une demande prématurée et mal contextualisée tire ces chiffres vers le bas.

Ensuite, segmentez par type. Un utilisateur peut vouloir des alertes de stock mais pas de newsletter. Stockez les préférences par catégorie dans votre backend, et abonnez aux topics correspondants côté client. Ne forcez jamais une catégorie qui n’a pas été explicitement acceptée.

Enfin, respectez les heures. Une notification à 3h du matin pour annoncer une promo est le moyen le plus efficace de perdre un utilisateur. Côté backend, refusez d’envoyer hors créneaux raisonnables, ou utilisez le champ delivery time de l’API FCM pour différer l’envoi.

Erreurs fréquentes

Erreur Cause Solution
Aucune notification reçue sur Android 13+ Permission POST_NOTIFICATIONS non demandée. Appeler messaging.requestPermission() au bon moment et vérifier le statut.
Notification foreground qui n’apparaît pas FCM n’affiche rien en foreground sur Android par défaut. Câbler flutter_local_notifications dans onMessage avec un channel.
Background handler jamais exécuté en release @pragma('vm:entry-point') manquant. Annoter le handler top-level. Pas de closure.
iOS : pas de notification du tout Certificat APNs absent ou capability non activée. Vérifier capability Xcode + clé .p8 chargée dans Firebase.
Token nul au démarrage Initialisation Firebase pas encore terminée. Toujours await Firebase.initializeApp() avant getToken().
Token qui change inopinément Comportement normal : réinstallation, restore, changement de SIM. Écouter onTokenRefresh et notifier le backend.

Tutoriels associés

Foire aux questions

FCM est-il vraiment gratuit ?

Oui, sans plafond connu pour la simple distribution de messages — Google ne facture pas l’envoi. Les fonctionnalités payantes sont autour : analytics avancées, A/B testing de notifications, programmation par cohorte. Pour une majorité d’applications, le tier gratuit suffit.

Peut-on envoyer une notification depuis le backend ?

Oui, c’est le mode de fonctionnement normal. Votre backend appelle l’API HTTP v1 FCM avec un service account JSON (généré dans la console Firebase) et envoie un JSON décrivant la notification + le token cible. Toutes les bibliothèques Firebase Admin (Node, Python, Go, Java, .NET) encapsulent cet appel.

Quelle différence entre notification message et data message ?

Un notification message contient un champ notification (titre, corps) qui est rendu automatiquement par le système — utile pour des alertes simples. Un data message ne contient que des paires clé-valeur, l’app gère l’affichage. Le data message est plus flexible mais plus délicat sur iOS où il faut content-available: 1 pour réveiller l’app en background.

Comment gérer la désinscription d’un utilisateur ?

Côté backend, supprimez le token de votre table devices au logout. Côté client, appelez FirebaseMessaging.instance.deleteToken() pour invalider le token côté FCM — important si l’appareil est partagé.

Que faire si la notification doit ouvrir un écran précis ?

Mettez l’identifiant cible dans le champ data de la notification : {"data": {"screen": "order_detail", "order_id": "123"}}. Lisez message.data['screen'] dans getInitialMessage() et dans onMessageOpenedApp, puis poussez la route correspondante via votre navigateur (go_router typiquement).

Faut-il un device physique pour tester FCM ?

Non, l’émulateur Android (avec Google Play Services installé — c’est le cas des images standard récentes) reçoit les notifications correctement. Pour iOS, le simulateur supporte les notifications push depuis Xcode 14 (mai 2022), à condition de tourner sur macOS 13+ avec une machine Apple Silicon ou T2, en environnement sandbox APNs uniquement. Les extensions de notification (Notification Service, Notification Content) restent limitées au device physique. Pour un test bout en bout fiable, l’iPhone physique reste l’option de référence.

Ressources officielles

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité