Prerequisites

To complete this guide, you will need the following:

  • Apple Developer membership (to obtain the required permissions to send push notifications).
  • A machine running MacOS to work on building the Flutter app for iOS devices.
  • Firebase account
  • Novu account.
  • Android Studio configured with Dart and Flutter plugins.
  • Xcode installed on your machine.

Check out the Flutter app code for this guide on GitHub.

Set up and Create a Flutter App

If you have an existing Flutter App, you can skip this step.

If you are creating a brand new Flutter app, please follow this guide.

Set up Firebase and FlutterFire

Please follow this guide to set up a Firebase project and integrate Firebase Cloud Messaging in your Flutter app.

Set Up Apple Integration

Please follow this guide to ensure that everything is set up on your iOS device to receive messages.

There is an optional section about allowing Notification Images. Follow the steps to install and activate the notification service extension. We’ll need it later in this guide to display images as part of the push notifications from FCM.

Set Up FCM Notification Permission & Registration Token

We’ll add code to initialize Firebase and ask the user’s permission to receive notifications.

Open up the lib/main.dart file in your Flutter app and replace everything with the code below:

import 'package:flutter/foundation.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; // FlutterFire's Firebase Cloud Messaging plugin

// Add Stream controller
import 'package:rxdart/rxdart.dart';

// for passing messages from event handler to the UI
final _messageStreamController = BehaviorSubject<RemoteMessage>();

('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
  print('Message data: ${message.data}');
  print('Message notification: ${message.notification?.title}');
  print('Message notification: ${message.notification?.body}');

}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // Request permission
  final messaging = FirebaseMessaging.instance;

  // Web/iOS app users need to grant permission to receive messages
  final settings = await messaging.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false,
    sound: true,
  );

  if (kDebugMode) {
    print('Permission granted: ${settings.authorizationStatus}');
  }

  // Register with FCM
  // It requests a registration token for sending messages to users from your App server or other trusted server environment.
  String? token = await messaging.getToken();

  if (kDebugMode) {
    print('Registration Token=$token');
  }

  // Set up foreground message handler
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    if (kDebugMode) {
      print('Handling a foreground message: ${message.messageId}');
      print('Message data: ${message.data}');
      print('Message notification: ${message.notification?.title}');
      print('Message notification: ${message.notification?.body}');
    }

    _messageStreamController.sink.add(message);
  });

  // Set up background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _lastMessage = "";

  _MyHomePageState() {
    _messageStreamController.listen((message) {
      setState(() {
        if (message.notification != null) {
          _lastMessage = 'Received a notification message:'
              '\nTitle=${message.notification?.title},'
              '\nBody=${message.notification?.body},'
              '\nData=${message.data}';
        } else {
          _lastMessage = 'Received a data message: ${message.data}';
        }
      });
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Last message from Firebase Messaging:',
                style: Theme.of(context).textTheme.titleLarge),
            Text(_lastMessage, style: Theme.of(context).textTheme.bodyLarge),
          ],
        ),
      ),
    );
  }
}

lib/main.dart

There are 4 sections to pay attention to:

1

The Request permission code block

This shows the notification permission prompt to the user when the user interacts with the app for the first time. If the user allows it, then we can get a token.

// Request permission
final messaging = FirebaseMessaging.instance;

// Web/iOS app users need to grant permission to receive messages
final settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);
2

Registering and obtaining the FCM token for the device

A device’s token is needed for the specific device to receive messages. The getToken() returns the registration token for the app instance on the device.

  String? token = await messaging.getToken();

  if (kDebugMode) {
    print('Registration Token=$token');
  }
3

The foreground message handler

This handler listens to when a push notification is sent from FCM to the device. If the app is in the foreground, then it prints out the notification title, body, messageId, and data properties to the console.

  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    if (kDebugMode) {
      print('Handling a foreground message: ${message.messageId}');
      print('Message data: ${message.data}');
      print('Message notification: ${message.notification?.title}');
      print('Message notification: ${message.notification?.body}');
    }

    _messageStreamController.sink.add(message);
  });
4

The background message handler

This handler listens to when a push notification is sent from FCM to the device. If the app is in the background, then it prints out the notification title, body, messageId, and data properties to the console.

  ...
  ('vm:entry-point')
  Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
    // If you're going to use other Firebase services in the background, such as Firestore,
    // make sure you call `initializeApp` before using other Firebase services.
    await Firebase.initializeApp();

    print("Handling a background message: ${message.messageId}");
    print('Message data: ${message.data}');
    print('Message notification: ${message.notification?.title}');
    print('Message notification: ${message.notification?.body}');

  }
  ...


  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

Now, run the Flutter app. Your device should be connected to your machine to enable the app to run on it.

Running the app to build on my device


Notification Permission Prompt


Flutter app running waiting to receive push notifications

Cloud Message Test

In your Firebase project, navigate to Engage section and click on Messaging.

Click on Create your first campaign and select Firebase Notification messages.

Enter the Notification title and the Nabigateotification text, and click on Send test message.

We must copy and paste this FCM registration token to confirm our device. You can find it logged as shown earlier in our editor.

Click on ‘Test’.

You should see the notification on your device!

Yaay! Push Notification from FCM on my device

Sign Up on Novu

Let’s use Novu to fire push notifications via FCM. First, sign up on Novu Cloud.

Next, immediately configure FCM as a Push channel provider.

You can also do this by heading straight to the Integrations Store. Click on Add a provider.

Connect FCM as a Push Channel provider

You only need to configure FCM with Novu with the Firebase Service Accounts private key.

To acquire the account key JSON file for your service account, follow this instructions:

  1. Select your project. Click the gear icon on the top of the sidebar.
  2. Head to project settings.
  3. Navigate to the Service accounts tab.
  4. Click Generate new private key, then confirm by clicking Generate key.
  5. Click Generate key to download the JSON file.
  6. Once the file is on your machine, paste the entire JSON file content in the Service Account field of the FCM provider in the Integration store on Novu’s web dashboard.

Make sure your service account key JSON content contains these fields:


{
  "type": "service_account",
  "project_id": "PROJECT_ID",
  "private_key_id": "PRIVATE_KEY_ID",
  "private_key": "PRIVATE_KEY",
  "client_email": "FIREBASE_ADMIN_SDK_EMAIL",
  "client_id": "CLIENT_ID",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "CLIENT_X509_CERT_URL"
}

Create a Notification Workflow

In Novu, creating a workflow means establishing a blueprint for sending notifications within your app. This unified structure ties together email, in-app messages, SMS, push notifications, and chat into one entity.

Each workflow has a unique name and identifier and includes customized content for various channels, using {{handlebars}} variables for personalization.

This ensures consistent platform notifications and allows dynamic adjustments for individual subscribers, scenarios, and use cases.

1

Navigate to 'Workflows' tab and click on 'Blank Workflow'

2

Add (Drag & Drop) the Push channel node to the workflow

3

Click on the node, and start to modify the content. Add a title and content. Once you are done, click on 'Update'

We need to fire notifications to subscribers. So, let’s create a subscriber!

Create A Subscriber & Update Credentials

A subscriber is essentially the recipient of the notifications sent through Novu’s system. When you create a subscriber, you’re setting up the necessary information for Novu to send targeted notifications to that individual.

You can see the list of subscribers in the Subscribers section of the Novu dashboard.

1

Create a subscriber


curl --location 'https://api.novu.co/v1/subscribers' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: ApiKey <YOUR_API_KEY>' \
--data-raw '{
"subscriberId": "584349343",
"firstName": "Flutter",
"lastName": "Boy",
"email": "flutterboy@gmail.com",
}'

2

Grab the FCM device token from your Editor log

3

Add Device Tokens to Subscriber by Updating Subscriber Credentials

Subscriber can have multiple device tokens

Here, you can locate the Provider_Identifier.


  curl --request PUT \
      --url https://api.novu.co/v1/subscribers/{subscriber_id}/credentials \
      --header 'Content-Type: application/json' \
      --header 'Authorization: ApiKey <YOUR_API_KEY>' \
      --data '{
          "credentials":{
            "deviceTokens": ["<insert-fcm-token>"]
        },
        "integrationIdentifier":"<Provider_Identifier>",
        "providerId":"fcm"
  }'

Update the Subscriber Credentials by adding the device tokens to the subscriber

Sending a Push Notification to a Device(Android/iOS) with Novu

To send a notification with Novu, you are actually triggering a notification workflow. You have the ability to test the trigger using the Novu UI or by calling Novu’s API.

Trigger a workflow via the API

Insert the trigger identifier as the name in the API request below:


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <REPLACE_WITH_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "<REPLACE_WITH_DATA>"
         },
         "payload": {}
       }'

Trigger a workflow via the Novu Dashboard

Add Push title and content

Trigger Workflow

Receive Triggered Push Workflow on Device

Push Notification Received on the Device

Dynamic Content

When we were creating the first workflow, we “Hardcoded” the content of the notification.

Now, we will determine the content when calling the API.

  1. In the workflow, we should change the values of the title and the body to {{title}} and {{body}}. That way, we could insert values through the payload of the API call.
  1. Add a payload object to the API call.

Insert the trigger identifier as the name in the API request below:


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <REPLACE_WITH_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "This title was set via the Payload",
           "body": "This body was set via the Payload"
         },
       }'

Push Notification received on device from Novu via FCM

Handling Data Type Messages

With FCM, you can send Notification and Data messages. We have handled sending notification messages. Let’s handle data messages.

Data messages contain user-defined custom key-value pairs. The information is encapsulated in the data key. The app can do whatever it pleases with the data once it’s received by the device.

Via the Trigger API call


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <REPLACE_WITH_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "This title was set via the Payload",
           "body": "This body was set via the Payload"
         },
         "overrides": {
          "fcm": {
            "data": {
              "subscription_status": "active",
              "renewed_by": "bisola"
            }
          }
         }
       }'

Push & Data Notification received on device in the foreground from Novu via FCM

Sound of a notification

You can use the name of a sound file in your app’s main bundle or in the Library/Sounds folder of your app’s container directory.

Specify the string “default” to play the system sound. Use it for regular notifications. For critical alerts, use the sound dictionary instead.

The code below works for iOS devices:


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <YOUR_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "Notification With Sound",
           "body": "Hello World from Novu"
         },
         "overrides": {
           "fcm": {
             "apns": {
               "payload": {
                 "aps": {
                   "sound": "default" // configure sound to the notification
                 }
               }
             }
           }
         }
       }'

For Android devices, use the following:


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <YOUR_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "Notification With Sound",
           "body": "Hello World from Novu"
         },
         "overrides": {
           "fcm": {
             "android": {
               "notification": {
                 "defaultSound": true // This uses the Android framework's default sound for the notification.
               }
             }
           }
         }
       }'

Priority for notification

If you omit this header, APNs (iOS) set the notification priority to 10.

  • Specify 10 to send the notification immediately.
  • Specify 5 to send the notification based on power considerations on the user’s device.
  • Specify 1 to prioritize the device’s power considerations over all other factors for delivery and prevent awakening the device.

curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <YOUR_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "apns-priority: 5",
           "body": "Novu's API"
         },
         "overrides": {
           "fcm": {
             "apns": {
               "headers": {
				          "apns-priority":"5" //priority for notifications
			          },
               "payload": {
                 "aps": {}
               },
               "fcm_options": {}
             }
           }
         }
       }'

For Android devices, specify high or normal for the priority of the notification message.


curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <YOUR_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "novu-push-workflow-for-flutter",
         "to": {
           "subscriberId": "584349343"
         },
         "payload": {
           "title": "priority",
           "body": "Novu's API"
         },
         "overrides": {
           "fcm": {
             "android": {
               "priority": "high"
             }
           }
         }
       }'

Sending Push Notification With Image

Up to this point, your notifications have all contained only text. But if you’ve received many notifications, you know that notifications can have rich content, such as images. It’d be great if your notifications showed users a nice image related to their content. Once again, Firebase makes this super simple.

To show an image in push notifications, you’ll need to create a Notification Service Extension. We handled it earlier in this guide during the Apple setup.

For reiteration, follow this guide to ensure it is properly set up for iOS devices.

When making the API call, we should include:

  • “mutable-content”: 1 inside the “aps” object.
  • ”image” in “fcm_options” object,
  • URL address of the image.

curl --location --request POST 'https://api.novu.co/v1/events/trigger' \
     --header 'Authorization: ApiKey <YOUR_API_KEY>' \
     --header 'Content-Type: application/json' \
     --data-raw '{
         "name": "untitled",
         "to": {
           "subscriberId": "12345678"
         },
         "payload": {
           "title": "This is a notification with an image",
           "body": "Check this out"
         },
         "overrides": {
           "fcm": {
             "apns": {
                "payload": {
                  "aps": {
                    "mutable-content": 1
                  }
                },
                "fcm_options": {
                  "image": "https://www.planetware.com/wpimages/2020/02/france-in-pictures-beautiful-places-to-photograph-eiffel-tower.jpg"
                  }
            }
          }
        }
    }'

Additional resources