๐Ÿ“ฉ firebase_notification_helper

A lightweight, easy-to-use Flutter helper package for Firebase Cloud Messaging (FCM), Local Notifications, and Firebase Cloud Function based remote push notification sending.

This package is designed to make notification integration extremely simple โ€” even for beginners.
It includes FCM token fetching, local notification display, foreground notification handling, background handler support, topic subscription, and remote push sending through Firebase Cloud Functions.


โœ… Summary of Everything Included in This Package

  • โœ”๏ธ Firebase Cloud Messaging setup
  • โœ”๏ธ Local notification support
  • โœ”๏ธ FCM token fetch
  • โœ”๏ธ Android notification channel creation
  • โœ”๏ธ Foreground notification listener
  • โœ”๏ธ Background message handler support
  • โœ”๏ธ Notification tap payload callback
  • โœ”๏ธ FCM token refresh callback
  • โœ”๏ธ Topic subscribe and unsubscribe
  • โœ”๏ธ Remote notification sending through Firebase Cloud Functions
  • โœ”๏ธ Send notification to token, multiple tokens, topic, or condition
  • โœ”๏ธ Beautiful full-page Flutter example UI
  • โœ”๏ธ Example app run instructions
  • โœ”๏ธ Security best practices
  • โœ”๏ธ Pub.dev-ready consistent formatting

Remote notification sending requires Firebase Cloud Functions or another trusted server environment.


โญ Features

  • ๐Ÿ”ฅ Fetch FCM Token
  • ๐Ÿ”” Show Local Notifications
  • ๐Ÿ“จ Send Remote Push Notification using Firebase Cloud Functions
  • ๐Ÿ“ก Send Notification to Topic
  • ๐Ÿ‘ฅ Send Notification to Multiple Tokens
  • ๐ŸŽฏ Send Notification to Condition
  • ๐Ÿš€ Foreground Message Handler Support
  • ๐ŸŒ™ Background Message Handler Support
  • ๐Ÿ‘† Notification Tap Callback
  • ๐Ÿ”„ Token Refresh Callback
  • ๐Ÿงช Full Example App Included
  • ๐Ÿ›ก Safer than client-side server key sending
  • ๐Ÿ“ฆ Clean and beginner-friendly API

๐Ÿ” Important Security Note

Do not put your Firebase server key or service account key inside a Flutter app.

Remote push notification sending should be done from a trusted environment, such as:

  • Firebase Cloud Functions
  • Your own backend server
  • Firebase Admin SDK

This package supports remote notification sending through Firebase Cloud Functions.


๐Ÿ“ฆ Installation

Add the package in pubspec.yaml:

dependencies:
  firebase_notification_helper: ^0.0.4

Then run:

flutter pub get

๐Ÿ”ฅ Firebase Setup

Before using this package, connect your Flutter app with Firebase.

Recommended setup:

dart pub global activate flutterfire_cli
flutterfire configure

For Android, make sure google-services.json exists here:

android/app/google-services.json

For the package example app, make sure google-services.json exists here:

example/android/app/google-services.json

๐Ÿค– Android Permission Setup

Add this permission in:

android/app/src/main/AndroidManifest.xml

Add it above the <application> tag:

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

โš™๏ธ Upgrade this android part in /android/build.gradle.kts

If your project needs manual Firebase setup, add Google Services classpath:

buildscript {
    repositories {
        google()
        mavenCentral()
    }

    dependencies {
        classpath("com.google.gms:google-services:4.4.2")
    }
}

โš™๏ธ Upgrade this android part in /android/app/build.gradle.kts

Add Google Services plugin:

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.google.gms.google-services") // REQUIRED
    id("dev.flutter.flutter-gradle-plugin")
}

If your project needs local notification desugaring support, add this:

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
        isCoreLibraryDesugaringEnabled = true
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_11.toString()
    }
}

dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}

๐Ÿš€ Initialization

Import the package:

import 'package:flutter/material.dart';
import 'package:firebase_notification_helper/firebase_notification_helper.dart';

Then initialize before runApp():

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await FirebaseNotificationHelper.initialize(
    config: const NotificationHelperConfig(
      debug: true,
      channelId: 'high_importance_channel',
      channelName: 'High Importance Notifications',
      channelDescription: 'Used for important notifications.',
      cloudFunctionName: 'sendNotification',
      cloudFunctionRegion: 'us-central1',
      requestPermissionOnInit: true,
      enableForegroundLocalNotification: true,
      handleInitialMessage: true,
      emitInitialToken: true,
    ),
    onNotificationTap: (payload) {
      debugPrint('Notification tapped: $payload');
    },
    onTokenChanged: (token) {
      debugPrint('FCM Token Changed: $token');
    },
  );

  runApp(const MyApp());
}

๐Ÿ”‘ Get FCM Token

final token = await FirebaseNotificationHelper.getToken();
debugPrint("FCM Token: $token");

๐Ÿ”” Show Local Notification

await FirebaseNotificationHelper.showLocalNotification(
  title: "Local Test",
  body: "This alert is from your device.",
  payload: {
    "screen": "home",
    "type": "local",
  },
);

๐Ÿ“ก Subscribe to Topic

await FirebaseNotificationHelper.subscribeToTopic("all_users");

๐Ÿ“ก Unsubscribe from Topic

await FirebaseNotificationHelper.unsubscribeFromTopic("all_users");

โ˜๏ธ Firebase Cloud Function Setup for Remote Notification

Remote push notification sending requires Firebase Cloud Functions.

Flow:

Flutter App
   โ†“
firebase_notification_helper
   โ†“
Firebase Cloud Function
   โ†“
Firebase Admin SDK
   โ†“
Firebase Cloud Messaging
   โ†“
Receiver Device

Initialize Firebase Functions:

firebase init functions

Choose:

JavaScript

๐Ÿ“ functions/package.json

Use this code:

{
  "name": "functions",
  "description": "Cloud Functions for firebase_notification_helper",
  "type": "module",
  "engines": {
    "node": "22"
  },
  "main": "index.js",
  "scripts": {
    "serve": "firebase emulators:start --only functions",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "dependencies": {
    "firebase-admin": "^14.0.0",
    "firebase-functions": "^7.2.5"
  },
  "devDependencies": {}
}

๐Ÿ“ functions/index.js

Use this Cloud Function code:

import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
import { HttpsError, onCall } from "firebase-functions/v2/https";

initializeApp();

const REGION = "us-central1";

/**
 * For testing: false
 * For production: true
 *
 * If true, user must be logged in with Firebase Auth.
 */
const REQUIRE_AUTH = false;

export const sendNotification = onCall(
  {
    region: REGION
  },
  async (request) => {
    try {
      if (REQUIRE_AUTH && !request.auth) {
        throw new HttpsError(
          "unauthenticated",
          "You must be logged in to send notification."
        );
      }

      const result = await sendFcmMessage(request.data || {});
      return result;
    } catch (error) {
      if (error instanceof HttpsError) {
        throw error;
      }

      throw new HttpsError(
        "internal",
        error?.message || "Failed to send notification."
      );
    }
  }
);

async function sendFcmMessage(input) {
  const target = input.target || {};

  const token = target.token || input.token;
  const tokens = target.tokens || input.tokens;
  const topic = target.topic || input.topic;
  const condition = target.condition || input.condition;

  const title = input.title;
  const body = input.body;
  const data = input.data || {};
  const androidChannelId = input.androidChannelId || "high_importance_channel";

  if (!title || !body) {
    throw new HttpsError(
      "invalid-argument",
      "title and body are required."
    );
  }

  const safeData = stringifyData(data);

  const baseMessage = {
    notification: {
      title: String(title),
      body: String(body)
    },
    data: safeData,
    android: {
      priority: "high",
      notification: {
        channelId: String(androidChannelId),
        sound: "default"
      }
    },
    apns: {
      payload: {
        aps: {
          sound: "default"
        }
      }
    }
  };

  if (token) {
    const response = await getMessaging().send({
      ...baseMessage,
      token: String(token)
    });

    return {
      success: true,
      type: "token",
      message: "Notification sent successfully.",
      response
    };
  }

  if (Array.isArray(tokens) && tokens.length > 0) {
    const cleanTokens = tokens
      .map((item) => String(item).trim())
      .filter((item) => item.length > 0);

    if (cleanTokens.length === 0) {
      throw new HttpsError(
        "invalid-argument",
        "tokens list is empty."
      );
    }

    if (cleanTokens.length > 500) {
      throw new HttpsError(
        "invalid-argument",
        "Maximum 500 tokens are allowed per request."
      );
    }

    const response = await getMessaging().sendEachForMulticast({
      ...baseMessage,
      tokens: cleanTokens
    });

    return {
      success: response.failureCount === 0,
      type: "tokens",
      message: "Multicast notification request completed.",
      successCount: response.successCount,
      failureCount: response.failureCount,
      responses: response.responses.map((item, index) => ({
        token: cleanTokens[index],
        success: item.success,
        messageId: item.messageId || null,
        error: item.error?.message || null
      }))
    };
  }

  if (topic) {
    const response = await getMessaging().send({
      ...baseMessage,
      topic: normalizeTopic(String(topic))
    });

    return {
      success: true,
      type: "topic",
      message: "Topic notification sent successfully.",
      response
    };
  }

  if (condition) {
    const response = await getMessaging().send({
      ...baseMessage,
      condition: String(condition)
    });

    return {
      success: true,
      type: "condition",
      message: "Condition notification sent successfully.",
      response
    };
  }

  throw new HttpsError(
    "invalid-argument",
    "Provide token, tokens, topic or condition."
  );
}

function stringifyData(data) {
  const safeData = {};

  Object.entries(data || {}).forEach(([key, value]) => {
    if (value === null || value === undefined) {
      safeData[key] = "";
    } else if (typeof value === "object") {
      safeData[key] = JSON.stringify(value);
    } else {
      safeData[key] = String(value);
    }
  });

  return safeData;
}

function normalizeTopic(topic) {
  return topic.trim().replace(/^\/topics\//, "");
}

๐Ÿš€ Deploy Cloud Function

Run:

firebase deploy --only functions:sendNotification

๐Ÿ“จ Send Remote Push Notification to Token

final result = await FirebaseNotificationHelper.sendRemoteNotification(
  target: FcmTarget.token(receiverFcmToken),
  title: "Hello!",
  body: "This is sent using firebase_notification_helper.",
  data: {
    "screen": "chat",
    "id": "123",
  },
);

debugPrint(result.toString());

๐Ÿ“จ Send Remote Push Notification to Topic

First subscribe the receiver device:

await FirebaseNotificationHelper.subscribeToTopic("all_users");

Then send:

final result = await FirebaseNotificationHelper.sendRemoteNotification(
  target: const FcmTarget.topic("all_users"),
  title: "New Update",
  body: "A new update is available.",
  data: {
    "screen": "home",
  },
);

debugPrint(result.toString());

๐Ÿ“จ Send Remote Push Notification to Multiple Tokens

final result = await FirebaseNotificationHelper.sendRemoteNotification(
  target: FcmTarget.tokens([
    token1,
    token2,
    token3,
  ]),
  title: "Group Notification",
  body: "This notification was sent to multiple devices.",
  data: {
    "type": "group_message",
  },
);

debugPrint(result.rawBody);

๐Ÿ“จ Send Remote Push Notification to Condition

final result = await FirebaseNotificationHelper.sendRemoteNotification(
  target: const FcmTarget.condition(
    "'news' in topics || 'updates' in topics",
  ),
  title: "Condition Notification",
  body: "This was sent using a topic condition.",
  data: {
    "screen": "news",
  },
);

debugPrint(result.rawBody);

๐Ÿงช Usage Example

Import:

import 'package:flutter/material.dart';
import 'package:firebase_notification_helper/firebase_notification_helper.dart';

Then use this code:

import 'package:flutter/material.dart';
import 'package:firebase_notification_helper/firebase_notification_helper.dart';

class NotificationSenderPage extends StatefulWidget {
  const NotificationSenderPage({super.key});

  @override
  State<NotificationSenderPage> createState() => _NotificationSenderPageState();
}

class _NotificationSenderPageState extends State<NotificationSenderPage> {
  final TextEditingController tokenController = TextEditingController();
  final TextEditingController topicController = TextEditingController(
    text: "all_users",
  );
  final TextEditingController titleController = TextEditingController(
    text: "Test Notification",
  );
  final TextEditingController bodyController = TextEditingController(
    text: "Hello from firebase_notification_helper!",
  );

  bool sendToTopic = false;
  bool loading = false;
  String response = "";

  @override
  void initState() {
    super.initState();
    _loadToken();
  }

  Future<void> _loadToken() async {
    final token = await FirebaseNotificationHelper.getToken();

    if (!mounted) return;

    setState(() {
      tokenController.text = token ?? "";
    });
  }

  Future<void> _sendNotification() async {
    final title = titleController.text.trim();
    final body = bodyController.text.trim();

    if (title.isEmpty || body.isEmpty) {
      setState(() {
        response = "Title and body are required.";
      });
      return;
    }

    if (sendToTopic && topicController.text.trim().isEmpty) {
      setState(() {
        response = "Topic is required.";
      });
      return;
    }

    if (!sendToTopic && tokenController.text.trim().isEmpty) {
      setState(() {
        response = "Target token is required.";
      });
      return;
    }

    setState(() {
      loading = true;
      response = "";
    });

    try {
      final result = await FirebaseNotificationHelper.sendRemoteNotification(
        target: sendToTopic
            ? FcmTarget.topic(topicController.text.trim())
            : FcmTarget.token(tokenController.text.trim()),
        title: title,
        body: body,
        data: {
          "screen": "home",
          "source": "example_app",
        },
      );

      if (!mounted) return;

      setState(() {
        response = result.rawBody;
      });
    } catch (e) {
      if (!mounted) return;

      setState(() {
        response = e.toString();
      });
    } finally {
      if (!mounted) return;

      setState(() {
        loading = false;
      });
    }
  }

  Future<void> _showLocalNotification() async {
    await FirebaseNotificationHelper.showLocalNotification(
      title: titleController.text.trim().isEmpty
          ? "Local Notification"
          : titleController.text.trim(),
      body: bodyController.text.trim().isEmpty
          ? "This is a local notification."
          : bodyController.text.trim(),
      payload: {
        "type": "local",
        "screen": "home",
      },
    );
  }

  Future<void> _subscribeTopic() async {
    await FirebaseNotificationHelper.subscribeToTopic(
      topicController.text.trim(),
    );

    setState(() {
      response = "Subscribed to topic: ${topicController.text.trim()}";
    });
  }

  @override
  void dispose() {
    tokenController.dispose();
    topicController.dispose();
    titleController.dispose();
    bodyController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Notification Sender"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _showLocalNotification,
        child: const Icon(Icons.notifications),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          SwitchListTile(
            value: sendToTopic,
            title: const Text("Send to Topic"),
            onChanged: (value) {
              setState(() {
                sendToTopic = value;
              });
            },
          ),

          const SizedBox(height: 12),

          if (sendToTopic)
            TextField(
              controller: topicController,
              decoration: const InputDecoration(
                labelText: "Topic Name",
                border: OutlineInputBorder(),
              ),
            )
          else
            TextField(
              controller: tokenController,
              maxLines: 4,
              decoration: const InputDecoration(
                labelText: "Target FCM Token",
                border: OutlineInputBorder(),
              ),
            ),

          const SizedBox(height: 12),

          TextField(
            controller: titleController,
            decoration: const InputDecoration(
              labelText: "Notification Title",
              border: OutlineInputBorder(),
            ),
          ),

          const SizedBox(height: 12),

          TextField(
            controller: bodyController,
            maxLines: 2,
            decoration: const InputDecoration(
              labelText: "Notification Body",
              border: OutlineInputBorder(),
            ),
          ),

          const SizedBox(height: 12),

          if (sendToTopic)
            OutlinedButton.icon(
              onPressed: _subscribeTopic,
              icon: const Icon(Icons.add_alert),
              label: const Text("Subscribe This Device To Topic"),
            ),

          const SizedBox(height: 12),

          ElevatedButton.icon(
            onPressed: loading ? null : _sendNotification,
            icon: loading
                ? const SizedBox(
                    width: 18,
                    height: 18,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.send),
            label: Text(loading ? "Sending..." : "Send Notification"),
          ),

          const SizedBox(height: 16),

          const Text(
            "Response:",
            style: TextStyle(fontWeight: FontWeight.bold),
          ),

          const SizedBox(height: 8),

          SelectableText(
            response.isEmpty ? "No response yet." : response,
          ),
        ],
      ),
    );
  }
}

โ–ถ๏ธ Example App Run Instructions

Go to example folder:

cd example

Run:

flutter clean
flutter pub get
flutter run

Before running, make sure this file exists:

example/android/app/google-services.json

โ— Common Issues

google-services.json is missing

Place the file here:

android/app/google-services.json

For example app:

example/android/app/google-services.json

cloud_functions and firebase_core version conflict

Use compatible versions:

dependencies:
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0
  flutter_local_notifications: ^17.0.0
  cloud_functions: ^5.6.2
  http: ^1.2.0

Notification not showing in foreground

Initialize with:

enableForegroundLocalNotification: true

Topic notification not received

Subscribe the receiver device first:

await FirebaseNotificationHelper.subscribeToTopic("all_users");

๐Ÿ“š API Overview

FirebaseNotificationHelper.initialize();

FirebaseNotificationHelper.getToken();

FirebaseNotificationHelper.deleteToken();

FirebaseNotificationHelper.requestPermission();

FirebaseNotificationHelper.showLocalNotification(
  title: "Title",
  body: "Body",
);

FirebaseNotificationHelper.subscribeToTopic("all_users");

FirebaseNotificationHelper.unsubscribeFromTopic("all_users");

FirebaseNotificationHelper.sendRemoteNotification(
  target: FcmTarget.token(token),
  title: "Title",
  body: "Body",
);

๐Ÿ“„ License

MIT License


๐Ÿ‘จโ€๐Ÿ’ป Author

Developed by Nafim Ahmed.