Introduzione

Le notifiche locali sono avvisi generati direttamente dal dispositivo, senza bisogno di un server remoto. Sono perfette per promemoria, timer, allarmi e notifiche legate a eventi che l'app conosce in anticipo. A differenza delle notifiche push (che richiedono FCM e un backend), le notifiche locali funzionano completamente offline.

In questa guida vediamo come integrare il pacchetto flutter_local_notifications, configurarlo su Android e iOS, mostrare notifiche immediate e programmate, e gestire i tap dell'utente.

Installazione e dipendenze

Aggiungi le dipendenze al pubspec.yaml:

dependencies:
  flutter_local_notifications: ^17.2.3
  timezone: ^0.9.4

Il pacchetto timezone è necessario per programmare notifiche in un fuso orario specifico, evitando comportamenti imprevisti con l'ora legale.

Configurazione Android

Su Android 13+ è necessario il permesso per le notifiche. Aggiungi nell'AndroidManifest.xml:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

Le ultime due servono per le notifiche programmate precise che sopravvivono al riavvio del dispositivo.

Configurazione iOS

Su iOS, nel file AppDelegate.swift, assicurati di registrare il plugin. Il permesso viene richiesto a runtime tramite il codice Dart che vedremo tra poco.

Inizializzazione del servizio

È buona pratica incapsulare la logica in una classe dedicata:

import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tzdata;

class NotificationService {
  static final _plugin = FlutterLocalNotificationsPlugin();

  static Future<void> init() async {
    tzdata.initializeTimeZones();

    const androidSettings =
        AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

    const settings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );

    await _plugin.initialize(
      settings,
      onDidReceiveNotificationResponse: _onTap,
    );
  }

  static void _onTap(NotificationResponse response) {
    final payload = response.payload;
    if (payload != null) {
      // Naviga o gestisci il payload
      print('Notifica toccata: $payload');
    }
  }
}

Chiama NotificationService.init() nel main() prima di runApp().

Richiedere i permessi

Su Android 13+ e iOS occorre richiedere esplicitamente il permesso:

static Future<void> requestPermissions() async {
  final android = _plugin.resolvePlatformSpecificImplementation<
      AndroidFlutterLocalNotificationsPlugin>();
  await android?.requestNotificationsPermission();

  final ios = _plugin.resolvePlatformSpecificImplementation<
      IOSFlutterLocalNotificationsPlugin>();
  await ios?.requestPermissions(alert: true, badge: true, sound: true);
}

Mostrare una notifica immediata

static Future<void> showNow({
  required String title,
  required String body,
}) async {
  const androidDetails = AndroidNotificationDetails(
    'canale_generale',
    'Notifiche generali',
    channelDescription: 'Canale per avvisi generici',
    importance: Importance.max,
    priority: Priority.high,
  );

  const details = NotificationDetails(
    android: androidDetails,
    iOS: DarwinNotificationDetails(),
  );

  await _plugin.show(0, title, body, details, payload: 'home');
}

Su Android il concetto di canale (channel) è obbligatorio dalla versione 8.0: raggruppa le notifiche per tipologia e consente all'utente di gestirne le preferenze.

Programmare una notifica

Per inviare una notifica in un momento futuro si usa zonedSchedule:

static Future<void> scheduleReminder({
  required int id,
  required String title,
  required String body,
  required DateTime dateTime,
}) async {
  await _plugin.zonedSchedule(
    id,
    title,
    body,
    tz.TZDateTime.from(dateTime, tz.local),
    const NotificationDetails(
      android: AndroidNotificationDetails(
        'canale_promemoria',
        'Promemoria',
        importance: Importance.max,
        priority: Priority.high,
      ),
      iOS: DarwinNotificationDetails(),
    ),
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    matchDateTimeComponents: DateTimeComponents.time,
  );
}

Alcune note importanti:

  • AndroidScheduleMode.exactAllowWhileIdle garantisce che la notifica scatti all'orario preciso anche in modalità Doze.
  • matchDateTimeComponents: DateTimeComponents.time rende la notifica ricorrente ogni giorno alla stessa ora. Ometti questo parametro per una notifica singola.

Annullare le notifiche

// Annulla una notifica specifica
static Future<void> cancel(int id) => _plugin.cancel(id);

// Annulla tutte le notifiche programmate
static Future<void> cancelAll() => _plugin.cancelAll();

Gestire il tap all'avvio dell'app

Se l'utente tocca una notifica mentre l'app è terminata, puoi recuperare i dettagli al lancio:

static Future<void> checkLaunchDetails() async {
  final details = await _plugin.getNotificationAppLaunchDetails();
  if (details?.didNotificationLaunchApp ?? false) {
    final payload = details!.notificationResponse?.payload;
    // Gestisci la navigazione iniziale
  }
}

Best practice

  • Usa ID univoci per ogni notifica programmata, così da poterle annullare selettivamente.
  • Crea canali distinti per categorie diverse (promemoria, offerte, avvisi): l'utente potrà gestirli separatamente nelle impostazioni di sistema.
  • Non abusare delle notifiche: un numero eccessivo porta l'utente a disattivarle o a disinstallare l'app.
  • Testa su dispositivi reali: gli emulatori possono comportarsi in modo diverso, soprattutto con le notifiche programmate e il risparmio energetico.

Conclusione

flutter_local_notifications è la soluzione di riferimento per gestire avvisi generati localmente in Flutter. Con una corretta gestione dei canali, dei permessi e del timezone puoi realizzare promemoria affidabili e ricorrenti che funzionano anche offline. Per notifiche inviate da un server, invece, la strada è quella delle push con FCM.