Marino Sabijan
 · Founder @ OmegaX Health

Foreground In-App Notifications - Custom Code Modification Guide

Greetings, fellow developers! Today, let's dive into enhancing the live feedback and notification systems of your projects. This is especially handy when dealing with scenarios that require real-time engagement, like game invitations or similar interactions.

First things first, we're going to break down the process into three distinct scenarios:

  1. App in the Foreground (when it's open and actively being used) - applicable to both web and mobile platforms.

  2. App in the Background or Closed - for web.

  3. App in the Background or Closed - for mobile.

Scenario 3 is a breeze to implement within the Flutter Flow platform, while scenario 2 is a bit trickier but doable (check out the upcoming guide for that). Our focus today? Scenario 1—when your app is right there in front of the user, and you want to pop up a dialog or trigger some action upon receiving a notification.

Buckle up, here's the breakdown:

  1. GitHub Integration Setup: Link your Flutter Flow project with GitHub

  2. Custom Code Development Branch: To modify the flutterflow code

  3. Tune into the Develop Branch: Open your project's develop branch (terminal: git checkout develop)

  4. Adjust the Code: Modify the code in lib/backend/push_notifications/push_notifications_util.dart

    • We're especially jazzing up the fcmTokenUserStream to also support web. This is where we store the FCM token for web notifications.

    final fcmTokenUserStream = authenticatedUserStream
        .where((user) => user != null)
        .map((user) => user!.reference.path)
        .distinct()
        .switchMap(getFcmTokenStream)
        .map(
          (userTokenInfo) => makeCloudCall(
            'addFcmToken',
            {
              'userDocPath': userTokenInfo.userPath,
              'fcmToken': userTokenInfo.fcmToken,
              'deviceType': kIsWeb
                  ? 'Web'
                  : Platform.isIOS
                      ? 'iOS'
                      : 'Android',
            },
          ),
        );
    
    Stream<UserTokenInfo> getFcmTokenStream(String userPath) {
      if (kIsWeb) {
        return FirebaseMessaging.instance
            .getToken()
            .asStream()
            .where((fcmToken) => fcmToken != null && fcmToken.isNotEmpty)
            .map((token) => UserTokenInfo(userPath, token!));
      } else {
        return Stream.value(!kIsWeb && (Platform.isIOS || Platform.isAndroid))
            .where((shouldGetToken) => shouldGetToken)
            .asyncMap<String?>(
                (_) => FirebaseMessaging.instance.requestPermission().then(
                      (settings) => settings.authorizationStatus ==
                              AuthorizationStatus.authorized
                          ? FirebaseMessaging.instance.getToken()
                          : null,
                    ))
            .switchMap((fcmToken) => Stream.value(fcmToken)
                .merge(FirebaseMessaging.instance.onTokenRefresh))
            .where((fcmToken) => fcmToken != null && fcmToken.isNotEmpty)
            .map((token) => UserTokenInfo(userPath, token!));
      }
    }

    Now, Web notifications are a go for the foreground. (For background notifications, stay tuned for a future post.)

  5. Now, let's modify the lib/backend/push_notifications/push_notifications_handler.dart:

    • Custom Notification Dialog Import: Begin by importing your personalized notification dialog pop-up. Customize the dialog according to your design and preference.

    • Message Listener Function Definition: This function will be your ear to the ground for foreground notifications.

    import '../../components/customNotificationDialog.dart';
    
    /////////////// defining the custom function for handling FOREGROUND notifications
    
      void messageListener(BuildContext context) {
        FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    // Handle multiple notifications for the same message
          if (_handledMessageIds.contains('${message.messageId}0000foreground')) {
            return;
          }
          _handledMessageIds.add('${message.messageId}0000foreground');
    
          print('Got a new message whilst in the foreground!');
    
    // ... Extract notification data ...
    
          String? title = message.notification?.title;
          print('Notification Title: $title');
    
          String? body = message.notification?.body;
          print('Notification Body: $body');
    
          String? image = isWeb
              ? message.notification?.web?.image
              : message.notification?.android?.imageUrl ??
                  message.notification?.apple?.imageUrl;
          print('Notification Image: $image');
    
          print('Message also contained data: ${message.data}');
    
    // Show a custom dialog and handle after closure
    
          showCustomNotificationDialog(context, image, title, body)
              .then((_) => _handlePushNotification(message));
        });
      }
    ///////////////
    • Now, we're circling back to initState to call our freshly minted function:

       @override
        void initState() {
          super.initState();
          handleOpenedPushNotification();
          /////////////// calling the function for FOREGROUND notifications
          messageListener(context);
          ///////////////
        }
  6. And now, the pièce de résistance, your CustomNotificationDialog. This gem of a code segment allows you to craft customized notification dialogs with finesse. Create a separate file in your Android Studio IDE, or you can even manifest it using Flutter Flow (but I still didn't figure out their context passing to custom components.)

    import 'package:flutter/material.dart';
    import 'package:auto_size_text/auto_size_text.dart';
    import 'package:just_audio/just_audio.dart';
    
    import '../flutter_flow/flutter_flow_theme.dart';
    import '../flutter_flow/flutter_flow_widgets.dart';
    
    class CustomNotificationDialog extends StatelessWidget {
      final String? image;
      final String? title;
      final String? body;
    
      CustomNotificationDialog({
        this.image,
        this.title,
        this.body,
      });
    
      @override
      Widget build(BuildContext context) {
        return Dialog(
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)),
          elevation: 0,
          backgroundColor: FlutterFlowTheme.of(context).secondaryBackground,
          child: Container(
            height: 400,
            width: 400,
            decoration: BoxDecoration(
              color: FlutterFlowTheme.of(context).primaryBackground,
              borderRadius: BorderRadius.circular(24.0),
              border: Border.all(
                color: FlutterFlowTheme.of(context).primaryBackground,
                width: 1.0,
              ),
            ),
            child: Column(
              children: <Widget>[
                Expanded(
                  child: ClipRRect(
                    borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(24.0),
                      topRight: Radius.circular(24.0),
                    ),
                    child: Image.network(
                      image ?? 'your_default_image_url',
                      height: 200,
                      width: double.infinity,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
                Padding(
                  padding: EdgeInsets.all(24.0),
                  child: Column(
                    children: <Widget>[
                      Text(
                        title ?? 'Notification',
                        style: FlutterFlowTheme.of(context).headlineLarge,
                        textAlign: TextAlign.center,
                      ),
                      SizedBox(height: 10.0),
                      AutoSizeText(
                        body ?? 'No message body provided',
                        maxLines: 2,
                        textAlign: TextAlign.center,
                        style: FlutterFlowTheme.of(context).labelMedium.override(
                              fontFamily: 'Arial',
                              fontWeight: FontWeight.w500,
                              lineHeight: 1.4,
                            ),
                      ),
                    ],
                  ),
                ),
                Padding(
                  padding: EdgeInsets.all(24.0),
                  child: FFButtonWidget(
                    onPressed: () async {
                      Navigator.of(context).pop();
                      // code for navigation or launching URL
                    },
                    text: 'Confirm',
                    options: FFButtonOptions(
                      height: 50.0,
                      padding: EdgeInsetsDirectional.fromSTEB(20.0, 0.0, 20.0, 0.0),
                      color: FlutterFlowTheme.of(context).primary,
                      textStyle: FlutterFlowTheme.of(context).titleSmall.override(
                            fontFamily: 'Arial',
                            color: Colors.black,
                            fontSize: 18.0,
                            fontWeight: FontWeight.w600,
                          ),
                      elevation: 3.0,
                      borderSide: BorderSide(
                        color: Colors.transparent,
                        width: 2.0,
                      ),
                      borderRadius: BorderRadius.circular(40.0),
                      hoverColor: FlutterFlowTheme.of(context).secondary,
                      hoverBorderSide: BorderSide(
                        color: FlutterFlowTheme.of(context).primary,
                        width: 2.0,
                      ),
                      hoverTextColor: Colors.black,
                      hoverElevation: 6.0,
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    Future<void> showCustomNotificationDialog(
        BuildContext context, String? image, String? title, String? body) async {
      final AudioPlayer player = AudioPlayer();
      await player.setAsset('assets/audios/sound_file_name.wav');
      await player.play();
      await showDialog(
        context: context,
        builder: (context) => CustomNotificationDialog(
          image: image,
          title: title,
          body: body,
        ),
      );
    }
    
    //// use this to show the dialog: showCustomNotificationDialog(context, image, title, body);

Voilà!

There you have it, a dynamic system for engaging your users with real-time notifications and feedback when the app is in the FOREGROUND.

If you encounter any bumps on the development road, remember you can always drop me a line at [email protected]—I'm here to help. Happy coding flowing! 🚀

35
26 replies