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:
App in the Foreground (when it's open and actively being used) - applicable to both web and mobile platforms.
App in the Background or Closed - for web.
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:
GitHub Integration Setup: Link your Flutter Flow project with GitHub
Custom Code Development Branch: To modify the flutterflow code
Tune into the Develop Branch: Open your project's develop branch (terminal: git checkout develop)
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.)
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); /////////////// }
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! 🚀