Skip to content

Instantly share code, notes, and snippets.

@Nidal-Bakir
Last active March 9, 2025 13:31
Show Gist options
  • Save Nidal-Bakir/539f9ce487764b4e4105819bb9962d6f to your computer and use it in GitHub Desktop.
Save Nidal-Bakir/539f9ce487764b4e4105819bb9962d6f to your computer and use it in GitHub Desktop.
Flutter notification service
// MIT License
//
// Copyright (c) 2025 Nidal Bakir
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:rxdart/rxdart.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path;
import 'package:dio/dio.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:string_validator/string_validator.dart';
import '../../theme/app_theme.dart';
import '../../utils/logger/logger.dart';
const _channelId = '';
const _channelDeception = '';
const _channelTitle = '';
const _topicKey = 'general';
class NotificationService {
factory NotificationService() {
return _instance ??= NotificationService._();
}
NotificationService._();
static NotificationService? _instance;
final _firebaseMessaging = FirebaseMessaging.instance;
final _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
var _didInitializeNotificationService = false;
Future<void> initialize() async {
if (_didInitializeNotificationService) {
Logger.w(
'You already initialized the notification service. '
'Ignoring the reinitialize request!',
);
return;
}
final settings = await _firebaseMessaging.requestPermission();
if (settings.authorizationStatus != AuthorizationStatus.authorized) {
return;
}
unawaited(_firebaseMessaging.subscribeToTopic(_topicKey));
await _initLocalNotificationPackage();
_startNotificationsListener();
_didInitializeNotificationService = true;
}
Future<void> _initLocalNotificationPackage() async {
await _flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(
const AndroidNotificationChannel(
_channelId,
_channelTitle,
description: _channelDeception,
ledColor: AppColors.kSecondary,
playSound: true,
showBadge: true,
audioAttributesUsage: AudioAttributesUsage.notification,
importance: Importance.max,
enableLights: true,
enableVibration: false,
),
);
const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_stat_notification_icon');
const iosInitializationSettings = DarwinInitializationSettings(
defaultPresentAlert: true,
defaultPresentBadge: true,
defaultPresentBanner: true,
defaultPresentList: true,
defaultPresentSound: true,
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
requestCriticalPermission: false,
requestProvisionalPermission: false,
);
const initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: iosInitializationSettings,
);
await _flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (details) {
if (details.payload != null) {
handleNotificationTap(json.decode(details.payload!) as Map<String, dynamic>);
}
},
);
}
bool _isNotificationListenerInitialized = false;
void _startNotificationsListener() {
if (_isNotificationListenerInitialized) {
Logger.w(
'You already started the notification Listener. '
'Ignoring the start request!',
);
return;
}
_isNotificationListenerInitialized = true;
// show notifications
FirebaseMessaging.onMessage.listen(_showNotification);
}
bool _isTapListenersInitialized = false;
Future<void> startTapNotificationsListener() async {
assert(_didInitializeNotificationService, 'Did you forget to initialize the notification service?');
if (_isTapListenersInitialized) {
return;
}
_isTapListenersInitialized = true;
final remoteInitialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (remoteInitialMessage != null) {
handleNotificationTap(remoteInitialMessage.data);
}
FirebaseMessaging.onMessageOpenedApp.listen((remoteMessage) {
handleNotificationTap(remoteMessage.data);
});
final notificationAppLaunchDetails = await _flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails != null) {
final payload = notificationAppLaunchDetails.notificationResponse?.payload;
if (payload != null) {
handleNotificationTap(json.decode(payload) as Map<String, dynamic>);
}
}
}
Future<void> _showNotification(RemoteMessage message) async {
UnreadNotificationCount().incrementUnreadNotificationCount();
Logger.i(message.toMap().toString(), tag: 'notification');
final notificationDetails = await _generateNotificationDetails(message);
await _flutterLocalNotificationsPlugin.show(
Random().nextInt(99999),
message.notification?.title,
message.notification?.body,
notificationDetails,
payload: json.encode(message.data),
);
}
Future<NotificationDetails> _generateNotificationDetails(RemoteMessage message) async {
String? imageURL;
Map<String, dynamic>? data;
File? imageFile;
try {
final dataStr = message.data['data'] as String? ?? '';
if (dataStr.isNotEmpty) {
data = Map<String, dynamic>.from(jsonDecode(dataStr) as Map? ?? {});
imageURL = data['image'] as String?;
}
} catch (e, s) {
Logger.e("message", error: e, stackTrace: s, tag: 'generateNotificationDetails');
}
if (imageURL?.isURL() ?? false) {
imageFile = await gDownloadAndSaveFileToCacheDir(imageURL!);
} else {
imageFile = null;
}
final DarwinNotificationDetails darwinNotificationDetails;
final StyleInformation androidNotificationStyleInformation;
if (imageFile != null) {
darwinNotificationDetails = DarwinNotificationDetails(
attachments: [DarwinNotificationAttachment(imageFile.path, hideThumbnail: true)],
);
final bitmapImage = FilePathAndroidBitmap(imageFile.path);
androidNotificationStyleInformation = BigPictureStyleInformation(
bitmapImage,
largeIcon: bitmapImage,
hideExpandedLargeIcon: true,
contentTitle: message.notification?.title ?? '',
summaryText: message.notification?.body ?? '',
htmlFormatContentTitle: true,
htmlFormatTitle: true,
htmlFormatContent: true,
htmlFormatSummaryText: true,
);
} else {
darwinNotificationDetails = DarwinNotificationDetails();
androidNotificationStyleInformation = BigTextStyleInformation(
message.notification?.body ?? '',
contentTitle: message.notification?.title ?? '',
htmlFormatBigText: true,
htmlFormatContentTitle: true,
htmlFormatTitle: true,
htmlFormatContent: true,
htmlFormatSummaryText: true,
);
}
final androidNotificationDetails = AndroidNotificationDetails(
_channelId,
_channelTitle,
channelDescription: _channelDeception,
importance: Importance.max,
styleInformation: androidNotificationStyleInformation,
priority: Priority.max,
);
return NotificationDetails(android: androidNotificationDetails, iOS: darwinNotificationDetails);
}
}
//-------------------------------------------------------------------------------------------------
Future<File?> gDownloadAndSaveFileToCacheDir(String fileURL) async {
try {
final cacheDir = await path_provider.getTemporaryDirectory();
final filePath = cacheDir.path + path.basename(fileURL);
await Dio().download(fileURL, filePath);
final file = File(filePath);
if (file.existsSync()) {
return file;
}
Logger.e('Error the downloaded file don not exists', error: 'File not exist after download');
return null;
} catch (e, s) {
Logger.e('Error while downloading image for notification', error: e, stackTrace: s);
}
return null;
}
//-------------------------------------------------------------------------------------------------
void handleNotificationTap(Map<String, dynamic> payload) {
final key = NotificationKeys.extractNotificationKeyFromPayload(payload);
if (key == null) {
return;
}
// TODO: handel notification tap
// ....
}
//-------------------------------------------------------------------------------------------------
class UnreadNotificationCount {
UnreadNotificationCount._();
static UnreadNotificationCount? _i;
factory UnreadNotificationCount() {
return _i ??= UnreadNotificationCount._();
}
int _count = 0;
// No need to close this stream controller as it will life as long as the app is open
late final _behaviorSubject = BehaviorSubject<int>.seeded(_count);
Stream<int> get unreadNotificationCountStream {
return _behaviorSubject.stream;
}
void setUnreadNotificationCount(int count) {
assert(count >= 0);
_count = count;
_behaviorSubject.add(_count);
Logger.i("Set unread notification count. New count: $_count", tag: 'UnreadNotificationCount');
}
void incrementUnreadNotificationCount([int incrementBy = 1]) {
assert(incrementBy >= 1);
_count += incrementBy;
_behaviorSubject.add(_count);
Logger.i("Increment unread notification count. New count: $_count", tag: 'UnreadNotificationCount');
}
}
//-------------------------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment