Feature Guide
Push Notification

Push Notification Module

The Push Notification module creates a complete notification system using Firebase Cloud Messaging and Flutter Local Notifications.

Generated Structure

lib/core/notifications/
├── models/
│   └── push_notification_model.dart
├── services/
│   ├── fcm_service.dart
│   └── local_notification_service.dart
└── notification_handler.dart

Key Components

1. Notification Model (push_notification_model.dart)

Defines the data structure for notifications:

class PushNotificationModel {
  final String title;
  final String body;
  final String? imageUrl;
  final String? payload;
  
  PushNotificationModel({
    required this.title,
    required this.body,
    this.imageUrl,
    this.payload,
  });
  
  factory PushNotificationModel.fromJson(Map<String, dynamic> json) {
    return PushNotificationModel(
      title: json['title'] ?? '',
      body: json['body'] ?? '',
      imageUrl: json['imageUrl'],
      payload: json['payload'],
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'title': title,
      'body': body,
      'imageUrl': imageUrl,
      'payload': payload,
    };
  }
}

2. FCM Service (fcm_service.dart)

Handles Firebase Cloud Messaging integration:

class FCMService {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
  final LocalNotificationService _localNotificationService;
  final List<Function(PushNotificationModel)> _onNotificationReceivedListeners = [];
 
  FCMService(this._localNotificationService);
 
  /// Initialize FCM service
  Future<void> initialize() async {
    await _requestPermissions();
    await _setupForegroundNotifications();
    await _setupBackgroundAndTerminatedNotifications();
    await _setupOnMessageOpenedApp();
 
    // Get FCM token
    String? token = await getToken();
    if (token != null) {
      debugPrint('FCM Token: $token');
    }
 
    // Listen for token refreshes
    _firebaseMessaging.onTokenRefresh.listen((newToken) {
      debugPrint('FCM Token refreshed: $newToken');
    });
  }
 
  /// Request notification permissions
  Future<void> _requestPermissions() async {
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      announcement: false,
      badge: true,
      carPlay: false,
      criticalAlert: false,
      provisional: false,
      sound: true,
    );
 
    debugPrint('FCM permission status: ${settings.authorizationStatus}');
  }
 
  /// Setup handling of foreground notifications
  Future<void> _setupForegroundNotifications() async {
    // Handle foreground messages
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      debugPrint('Got a message whilst in the foreground!');
      debugPrint('Message data: ${message.data}');
 
      if (message.notification != null) {
        debugPrint('Message also contained a notification: ${message.notification}');
 
        final notification = PushNotificationModel(
          title: message.notification?.title ?? 'New Notification',
          body: message.notification?.body ?? '',
          payload: json.encode(message.data),
        );
 
        // Show local notification
        _localNotificationService.showNotification(notification);
 
        // Notify listeners
        _notifyListeners(notification);
      }
    });
  }
 
  // Other methods for handling notifications...
}

3. Local Notification Service (local_notification_service.dart)

Manages on-device notifications:

class LocalNotificationService {
  final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();
 
  final List<Function(String?)> _onNotificationTapListeners = [];
 
  /// Initialize local notification service
  Future<void> initialize() async {
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');
 
    final DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );
 
    final InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );
 
    await _flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
    );
  }
 
  /// Show a notification
  Future<void> showNotification(PushNotificationModel notification) async {
    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'high_importance_channel',
      'High Importance Notifications',
      importance: Importance.max,
      priority: Priority.high,
    );
 
    const DarwinNotificationDetails iOSPlatformChannelSpecifics =
        DarwinNotificationDetails(
      presentAlert: true,
      presentBadge: true,
      presentSound: true,
    );
 
    const NotificationDetails platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: iOSPlatformChannelSpecifics,
    );
 
    await _flutterLocalNotificationsPlugin.show(
      notification.hashCode,
      notification.title,
      notification.body,
      platformChannelSpecifics,
      payload: notification.payload,
    );
  }
 
  /// Handle notification response when tapped
  void _onDidReceiveNotificationResponse(NotificationResponse response) {
    final String? payload = response.payload;
 
    // Notify listeners
    for (var listener in _onNotificationTapListeners) {
      listener(payload);
    }
  }
 
  // Other methods...
}

4. Notification Handler (notification_handler.dart)

Provides a unified API for the app:

class NotificationHandler {
  late final FCMService _fcmService;
  late final LocalNotificationService _localNotificationService;
  
  /// Initialize notification services
  Future<bool> initialize() async {
    try {
      // Setup local notifications first
      _localNotificationService = LocalNotificationService();
      await _localNotificationService.initialize();
 
      // Setup FCM service
      _fcmService = FCMService(_localNotificationService);
      await _fcmService.initialize();
 
      // Register background message handler
      FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
 
      return true;
    } catch (e) {
      debugPrint('Error initializing notification services: $e');
      return false;
    }
  }
 
  /// Get FCM token
  Future<String?> getFCMToken() async {
    try {
      return await _fcmService.getToken();
    } catch (e) {
      debugPrint('Error in getFCMToken: $e');
      return 'Error: $e';
    }
  }
  
  /// Show a local notification
  Future<void> showLocalNotification(PushNotificationModel notification) async {
    await _localNotificationService.showNotification(notification);
  }
  
  /// Subscribe to a FCM topic
  Future<void> subscribeToTopic(String topic) async {
    await _fcmService.subscribeToTopic(topic);
  }
  
  /// Unsubscribe from a FCM topic
  Future<void> unsubscribeFromTopic(String topic) async {
    await _fcmService.unsubscribeFromTopic(topic);
  }
  
  /// Add listener for notification received
  void addOnNotificationReceivedListener(Function(PushNotificationModel) listener) {
    _fcmService.addOnNotificationReceivedListener(listener);
  }
  
  /// Add listener for notification tap
  void addOnNotificationTapListener(Function(String?) listener) {
    _localNotificationService.addOnNotificationTapListener(listener);
  }
}
 
/// Global notification handler instance
final notificationHandler = NotificationHandler();

5. Background Message Handler

Handles notifications when the app is in the background:

@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // Need to initialize Firebase before using it
  await Firebase.initializeApp();
 
  debugPrint('Handling background message: ${message.messageId}');
 
  // Initialize FlutterLocalNotificationsPlugin and show notification
  // ...
}

How to Use the Push Notification System

Initialize notifications in your main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Initialize Firebase
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  // Initialize notification services
  await notificationHandler.initialize();
  
  runApp(const App());
}

Send a test notification:

final notification = PushNotificationModel(
  title: 'Test Notification',
  body: 'This is a test notification from your app',
  payload: '{"type": "test", "id": "123"}',
);
 
await notificationHandler.showLocalNotification(notification);

Listen for notification taps:

@override
void initState() {
  super.initState();
  
  // Listen for notification taps
  notificationHandler.addOnNotificationTapListener(_onNotificationTap);
}
 
void _onNotificationTap(String? payload) {
  if (payload != null) {
    try {
      final data = json.decode(payload);
      // Handle notification tap
    } catch (e) {
      debugPrint('Error parsing payload: $e');
    }
  }
}
 
@override
void dispose() {
  notificationHandler.removeOnNotificationTapListener(_onNotificationTap);
  super.dispose();
}

Customizing the Push Notification System

  1. Custom Notification Channels: Modify the Android notification channels in local_notification_service.dart
  2. Custom Notification Sounds: Add sound files to your project and reference them in the notification details
  3. Actionable Notifications: Add buttons or quick reply options to notifications

Example of creating a notification with action buttons:

// In local_notification_service.dart
Future<void> showNotificationWithActions(PushNotificationModel notification) async {
  // For Android
  final androidPlatformChannelSpecifics = AndroidNotificationDetails(
    'high_importance_channel',
    'High Importance Notifications',
    importance: Importance.max,
    priority: Priority.high,
    actions: <AndroidNotificationAction>[
      AndroidNotificationAction(
        'accept',
        'Accept',
        showsUserInterface: true,
      ),
      AndroidNotificationAction(
        'reject',
        'Reject',
        showsUserInterface: true,
      ),
    ],
  );
 
  // For iOS
  final iOSPlatformChannelSpecifics = DarwinNotificationDetails(
    presentAlert: true,
    presentBadge: true,
    presentSound: true,
    categoryIdentifier: 'actionableNotification',
  );
 
  final platformChannelSpecifics = NotificationDetails(
    android: androidPlatformChannelSpecifics,
    iOS: iOSPlatformChannelSpecifics,
  );
 
  await _flutterLocalNotificationsPlugin.show(
    notification.hashCode,
    notification.title,
    notification.body,
    platformChannelSpecifics,
    payload: notification.payload,
  );
}

Common Module Integration

All these modules work together in the FlutterBunnyScreen demonstration screen that Flutter Bunny CLI generates. This screen showcases:

  1. Theme switching with your chosen state management
  2. Language selection and switching
  3. Push notification testing

The screen provides a practical example of how to use all these modules together in your application, making it an excellent learning resource.