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.exactAllowWhileIdlegarantisce che la notifica scatti all'orario preciso anche in modalità Doze.matchDateTimeComponents: DateTimeComponents.timerende 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.
