Introduzione

Le notifiche push sono uno strumento fondamentale per coinvolgere gli utenti di un'app mobile: avvisano di nuovi messaggi, promozioni o aggiornamenti anche quando l'app è chiusa. In Flutter, la soluzione più diffusa e gratuita è Firebase Cloud Messaging (FCM), integrabile tramite i pacchetti firebase_core e firebase_messaging.

In questa guida vedremo come configurare FCM, ricevere notifiche nei diversi stati dell'app (foreground, background, terminata) e mostrare notifiche locali con flutter_local_notifications.

Configurazione iniziale

Aggiungi le dipendenze al tuo pubspec.yaml:

dependencies:
  firebase_core: ^3.6.0
  firebase_messaging: ^15.1.3
  flutter_local_notifications: ^18.0.1

Dopo aver creato un progetto su Firebase Console, usa la FlutterFire CLI per generare la configurazione:

dart pub global activate flutterfire_cli
flutterfire configure

Questo comando crea il file firebase_options.dart e configura automaticamente Android e iOS.

Inizializzazione di Firebase

Inizializza Firebase nel main() prima di avviare l'app:

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());
}

Richiedere i permessi

Su iOS (e su Android 13+) è obbligatorio richiedere il permesso all'utente per ricevere notifiche:

import 'package:firebase_messaging/firebase_messaging.dart';

Future<void> requestPermission() async {
  final messaging = FirebaseMessaging.instance;
  final settings = await messaging.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    print('Permesso notifiche concesso');
  }
}

Ottenere il token FCM

Ogni dispositivo possiede un token univoco che identifica la destinazione delle notifiche. Va inviato al tuo backend per indirizzare i messaggi:

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

// Ascolta i rinnovi del token
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
  // Invia il nuovo token al backend
});

Gestire i messaggi nei tre stati

FCM si comporta diversamente a seconda dello stato dell'app.

App in foreground

Quando l'app è aperta, le notifiche non vengono mostrate automaticamente: dobbiamo gestirle noi, tipicamente mostrando una notifica locale.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  final notification = message.notification;
  if (notification != null) {
    showLocalNotification(
      notification.title ?? '',
      notification.body ?? '',
    );
  }
});

App in background o terminata (tap sulla notifica)

Quando l'utente tocca una notifica con l'app in background:

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  // Naviga verso una schermata specifica
  final route = message.data['route'];
  if (route != null) {
    navigatorKey.currentState?.pushNamed(route);
  }
});

Se l'app era completamente chiusa, recupera il messaggio iniziale:

final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
  // Gestisci la navigazione di avvio
}

Handler in background

Per elaborare messaggi anche quando l'app non è in foreground, registra un handler top-level (fuori da qualsiasi classe):

@pragma('vm:entry-point')
Future<void> firebaseBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Messaggio in background: ${message.messageId}');
}

void main() {
  FirebaseMessaging.onBackgroundMessage(firebaseBackgroundHandler);
  // ...
}

L'annotazione @pragma('vm:entry-point') è essenziale: impedisce al tree-shaking di rimuovere la funzione nelle build release.

Notifiche locali in foreground

Configuriamo flutter_local_notifications per mostrare le notifiche quando l'app è aperta:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final localNotifications = FlutterLocalNotificationsPlugin();

Future<void> initLocalNotifications() async {
  const androidSettings =
      AndroidInitializationSettings('@mipmap/ic_launcher');
  const iosSettings = DarwinInitializationSettings();
  const settings = InitializationSettings(
    android: androidSettings,
    iOS: iosSettings,
  );
  await localNotifications.initialize(settings);
}

Future<void> showLocalNotification(String title, String body) async {
  const androidDetails = AndroidNotificationDetails(
    'high_importance_channel',
    'Notifiche importanti',
    importance: Importance.max,
    priority: Priority.high,
  );
  const details = NotificationDetails(android: androidDetails);
  await localNotifications.show(0, title, body, details);
}

Su Android è inoltre consigliato creare il canale di notifica con la stessa id usata nei dettagli, altrimenti su Android 8+ le notifiche potrebbero non apparire.

Best practice

  • Richiedi i permessi al momento giusto: non al primo avvio, ma quando l'utente comprende il valore delle notifiche.
  • Usa i data message oltre ai notification message per veicolare informazioni di routing.
  • Salva e aggiorna il token sul backend, gestendo anche onTokenRefresh.
  • Testa tutti gli stati: foreground, background e app terminata si comportano diversamente.
  • Configura i canali su Android per dare all'utente controllo granulare.

Conclusioni

Integrare le notifiche push con Firebase Cloud Messaging in Flutter richiede pochi passaggi, ma è fondamentale gestire correttamente i diversi stati dell'app e combinare FCM con le notifiche locali per un'esperienza coerente. Con i frammenti visti in questa guida hai una base solida e pronta per la produzione.