In Flutter, how do we use Firebase Messaging onBackgroundMessage to create a notification, using flutter_local_notifications? - firebase

We are working on an encrypted chat application where we use Firebase Messaging for data notifications. Some client-side logic needs to be done upon receiving a data notification, before showing an actual notification to the user. For example, a phone number will have to be translated to a local contact name. This translation is done by lookup with a map that is already available globally.
The data notifications are received just fine and the onBackgroundMessage callback is called as well. However, it seems impossible to access any kind of state from the onBackgroundMessage function. For example, printing the phone number of the logged in user returns null.
Printing this same global variable from the onMessage callback works just fine.
Running flutter_local_notifications from onMessage works fine, but again, does not work at all from onBackgroundMessage as 'no implementation could be found for the method .show()'. At the moment, it claims that flutterLocalNotificationsPlugin is null, which it isn't really.
It seems to us that onBackgroundMessage has no access to anything the app provides, as soon as the app is backgrounded. Something has to be done to make some of the scope/context available to the background process. For now, that would mainly be the flutter_local_notifications plugin in its entirety, as well as the local contacts list to translate phone number to name.
Has anyone got any idea how to do this?
Here is some of the code:
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
final _chatRepository = ChatRepository();
Future<dynamic> backgroundMessageHandler(Map<String, dynamic> message) async {
if(message.containsKey('data')) {
await _showNotification(message);
return Future<void>.value();
}
}
Future _showNotification(message) async {
List<String> numbers = [];
numbers.add(message['data']['sender']);
var name = await _chatRepository.translatePhoneNumbersToChatName(numbers);
var androidPlatformChannelSpecifics = new AndroidNotificationDetails(
'channel id', 'channel name', 'channel description',
importance: Importance.Max, priority: Priority.High);
var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
var platformChannelSpecifics = new NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(
0,
name,
message['data']['body'],
platformChannelSpecifics,
payload: message['data']['body'],
);
}
class NotificationHandler {
final FirebaseMessaging fcm = FirebaseMessaging();
StreamSubscription iosSubscription;
String deviceToken = "";
Future<void> initialize() async {
flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
var initializationSettingsAndroid =
new AndroidInitializationSettings('#mipmap/ic_launcher');
var initializationSettingsIOS = new IOSInitializationSettings(onDidReceiveLocalNotification: onDidReceiveLocalNotification);
var initializationSettings = new InitializationSettings(initializationSettingsAndroid, initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: onClickNotification);
fcm.configure(
onMessage: (Map<String, dynamic> message) async {
if(message.containsKey('data')) {
print(message);
_showNotification(message);
}
},
onBackgroundMessage: Platform.isIOS
? null
: backgroundMessageHandler,
onLaunch: (Map<String, dynamic> message) async {
if(message.containsKey('data')) {
print(message);
_showNotification(message);
}
},
onResume: (Map<String, dynamic> message) async {
if(message.containsKey('data')) {
print(message);
_showNotification(message);
}
},
);
_updateDeviceToken();
}
.
.
.
Of course, the initialize above is called early on in the application lifecycle.

class NotificationHandler {
static final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); // make it a static field of the class
// ...
}
Future _showNotification(message) async {
// ...
await NotificationHandler.flutterLocalNotificationsPlugin.show( // access it
// ...
);
}
Hope this works for you.

This plugin explains it all better than I could, but it just so happens that the background is a completely different isolate/context and thus it has no access to any plugins if they use an old (pre Flutter 12) API.
https://pub.dev/packages/android_alarm_manager#flutter-android-embedding-v1
Embedding v1 requires you to register any plugins that you want to access from the background. Doing this makes it flutter_local_notifications work properly.
Unfortunately, FCM docs are heavily lacking.

Related

Flutter FCM data only payload notification is not received when app is terminated

I'm using firebase_messaging: ^11.2.10 and flutter_local_notifications: ^9.4.0 to show notifications. I am sending data only notifications without notification title or body. Most of the time it works, but when the app is terminated and not used for a longer period, notifications won't show until you wake or unlock the phone.
Here is the case:
I subscribe to the Firebase background messages.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(MessagingService.subscribeToFCMBackground);
await NotificationService().init();
runApp(const MyApp());
);
}
Inside my MessagingService I get the payload data and there it shows the notification based on the value under the "type" key. Looks like this:
static Future<void> subscribeToFCMBackground(RemoteMessage message) async {
await Firebase.initializeApp();
NotificationService().showNotificationByType(_getNotificationTypeFromMessage(message));
}
static NotificationType _getNotificationTypeFromMessage(final RemoteMessage message) {
final String notificationType = message.data['type'] as String;
return notificationTypeEnumMap[notificationType]!;
}
Inside the NotificationService I create notification channel, place it inside of NotificationDetails object and pass it to showNotification method. Here is an example of android channel:
final AndroidNotificationDetails _androidNotificationDetails = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Channel',
channelDescription: 'Very important notifications',
playSound: true,
sound: RawResourceAndroidNotificationSound(soundFileName),
priority: Priority.max,
importance: Importance.max,
);
...
final NotificationDetails platformChannelSpecifics = NotificationDetails(
android: _androidNotificationDetails,
iOS: _iOSNotificationDetails,
);
...
await flutterLocalNotificationsPlugin.show(
0,
'Notification title',
'Notification body',
platformChannelSpecifics,
);
Also I added this to the Android Manifest file:
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />
Do you guys have any idea how to solve this problem? I would really appreciate any advice or a solution. Thank you!

Flutter - Firebase Cloud Messaging Navigation in onLaunch doesn't work

I am building an app which receives push notifications using FCM.
I want to route to a specific screen when a notification is clicked (for example, the user's profile).
On Android, it works perfectly fine when the app is just closed (and not "killed"), but when the app is terminated ("killed") it is not working.
On iOS, it doesn't work at all.
I am implementing it life this:
NotificationsHandler:
class NotificationsHandler {
static final NotificationsHandler instance = NotificationsHandler();
final _fcm = FirebaseMessaging();
void onBackgroundNotificationRecevied({Function onReceived}) {
_fcm.configure(
onResume: (message) => onReceived(message),
onLaunch: (message) => onReceived(message),
);
}
}
myMainScreen's initState:
#override
void initState() {
NotificationsHandler.instance.onBackgroundNotificationRecevied(
onReceived: (message) async {
final userId = message['data']['userId'];
final user = this.users.firstWhere((currentUser) => currentUser.id == userId);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserProfileScreen(
user,
),
),
);
}
);
super.initState();
}
Code for sending the notifications (through an external React admin panel):
const payload = {
notification: {
title: `myTitle`,
body: `My message`,
sound: "default",
badge: "1",
click_action: "FLUTTER_NOTIFICATION_CLICK",
},
data: {
click_action: 'FLUTTER_NOTIFICATION_CLICK',
userId: myUserId,
},
};
const options = {
priority: 'high',
timeToLive: 60 * 60 * 24
};
admin.messaging().sendToTopic('myTopic', payload, options);
Does anyone know why it isn't working?
Thank you!
You can try to use getInitialMessage instead of onLaunch. I believe this will do what you want as documentation indicated the following lines:
This should be used to determine whether specific notification interaction should open the app with a specific purpose (e.g. opening a chat message, specific screen etc).
#override
void initState() {
super.initState();
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage message) {
if (message != null) {
Navigator.pushNamed(context, '/message', arguments: MessageArguments(message, true));
}
});
}
I assume that you're using firebase_messaging package.
iOS
If you're testing it on simulator, it won't work. It's stated in the documentation that:
FCM via APNs does not work on iOS Simulators. To receive messages & notifications a real device is required.
Android
On Android, if the user force quits the app from device settings, it must be manually reopened again for messages to start working.
More info here.
Based on my experience, I remember that onLaunch Callback function fires right after execute main function, even before the initstate method.
What I did was locate service class using service locator(e.g get_it) at main function before runApp() then onLaunch Callback set initial configuration so that your App can use it's value.
For example
final getIt = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
getIt.registerSingleton<Configurator>(Configurator());///start configuration service
FirebaseMessagingService.initialise()///start firebase messaging service
runApp();
}
...
class FirebaseMessagingService {
final FirebaseMessaging _fcm;
FirebaseMessagingService.initialise() : _fcm = FirebaseMessaging() {
if (Platform.isIOS) {
_fcm.requestNotificationPermissions(IosNotificationSettings());
}
_fcm.configure(
...
onLaunch: _launchMessageHandler,
);
}
}
//top-level function or static method
_launchMessageHandler(Map<String, dynamic> message) async {
//some parsing logic
...
getIt<Configurator>().setInitialConfig(parsed_data);
}
...
//then
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final config = getIt<Configurator>().config;
//do something
}};
You will have to implement those whole settings but it's flow is like above roughly.
I assume your trouble is more towards navigating to another screen upon clicking the notification.
If that is the case create a class for routing.
an example would be as below:
class Navigator{
GlobalKey<NavigatorState> _navigator;
/// Singleton getter
static Navigator get instance => _instance ??= Navigator._();
/// Singleton Holder
static Navigator _instance;
/// Private Constructor
Navigator._() {
_navigator = GlobalKey<NavigatorState>();
}
GlobalKey<NavigatorState> get navigatorKey => _navigator;
Future<dynamic> navigateTo(String routeName, [dynamic arguments]) =>
navigatorKey.currentState.pushNamed(routeName, arguments: arguments);
Now comes the screen/pages
class CustomRoutes {
const CustomRoutes._();
factory CustomRoutes() => CustomRoutes._();
static const String HomeRoute = 'HomeRoute';
...
...
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case CustomRoutes.HomeRoute:
return MaterialPageRoute(builder: (_) => HomePage());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(child: Text('No path for ${settings.name}'))));
}
}
}
So if u wish to go to HomePage you can just invoke
await Navigator.instance.navigateTo(CustomRoutes.HomeRoute, someArguments)
Do remember to register the globalkey to your materialapp
MaterialApp(
...
...
navigatorKey: Navigator.instance.navigatorKey
...);

How to show a push-notification when the application is open(foreground), when OnMessage is triggered?

Im use Flutter and Firebase Messaging.
I im configure Firebase like in example: firebaseMessaging.configure(
onMessage: ...
onLaunch: ...
onResume: ...
)
But i wanna see push-notification even when app is open.
Roughly speaking onMessage should work like onResume. How can i do this?
onMessage: (Map<String, dynamic> message) async {
showNotification(message);
print('on message $message');
}
showNotification(Map<String, dynamic> msg) async {
var android = new AndroidNotificationDetails(
'your channel id',//channel id
"your channel name",//channel name
"your channel description",//channel desc todo set all this right
icon: 'mipmap/launcher_icon'//add your icon here
);
var iOS = new IOSNotificationDetails();
var platform = new NotificationDetails(android, iOS);
await flutterLocalNotificationsPlugin
.show(0, msg['notification']['title'], msg['notification']['body'], platform);
}
I used flutter_local_notifications: ^1.2.2 to show local notification foreground.
Additionally, if you are implementing for IOS don't forget to ask for notification permission.

Flutter firebase push notification not routing to specific page

I am trying to navigate to a specific page when a notification is clicked. The onResume and onMessage callbacks are invoked when I click on the notification and I can see the message in the log screen. However, when I try to navigate to a specific page, I am not able to do that and there is no error message in the log too. P.S. When I used a Navigator key to access the state of the context(since in initState, the navigator cannot be used) I got an error saying no context to build. What is the mistake ??
I have tried Navigator.push, Calling a method and routing from within that method, used navigator key.
void initState() {
messaging.configure(
onMessage: (Map<String, dynamic> message) async {
print('onMessage: $message');
Navigator.of(context).push(
MaterialPageRoute<BuildContext>(builder: (_) => PageContent(value:1)));
},
onLaunch: (Map<String, dynamic> message) async {
print('onLaunch: $message');
Navigator.of(context).push(
MaterialPageRoute<BuildContext>(builder: (_) => PageContent(value:2)));
},
onResume: (Map<String, dynamic> message) async {
print('onResume:- This is the message $message');
Navigator.of(context).push(
MaterialPageRoute<BuildContext>(builder: (_) => MoviesList()));
},
);
I expect the code to be loaded when the notification is tapped and route to a new page( MoviesList or PageContent in my case). But only my home screen is visible.
Context is not available in init state
I came across this issue and get resolved using redux concepts
add a key in a global state like appNavigator
sample code for global app state (app_state.dart),
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:flutter/material.dart' hide Builder;
part 'app_state.g.dart';
abstract class AppState implements Built<AppState, AppStateBuilder> {
factory AppState([AppStateBuilder updates(AppStateBuilder builder)]) =
_$AppState;
AppState._();
static AppState initState() {
return new AppState((AppStateBuilder b) {
b
..appNavigator = new GlobalKey<NavigatorState>(debugLabel: 'debugLabel')
.. isLoggedIn = false
..isLoading = false;
});
}
// Never change this key through out the app lifecycle
GlobalKey<NavigatorState> get appNavigator;
// login state ***************************************************************************
bool get isLoggedIn;
// indicates loading state ***************************************************************************
bool get isLoading;
}
dispatch an action onMessage received from the notification like
onMessage: (Map<String, dynamic> message) async {
print('onMessage: $message');
store.dispatch(new RedirectUserOnNotification());
},
and in middleware route to a specific page with conditions validation as you needed.
void redirectuser(Store<AppState> store, RedirectUserOnNotification action,
NextDispatcher next) async {
store.state.appNavigator.currentState.pushNamed(someRouteName);
next(action);
}
Note: I have used build_value concepts in a model file

How to refresh firebase token on Flutter?

I have a Flutter app that uses Firebase messaging to delivery notifications.
This is the base code, it does nothing special, besides saving the token on my DB.
FirebaseMessaging _firebaseMessaging = new FirebaseMessaging();
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) {
},
onResume: (Map<String, dynamic> message) {
},
onLaunch: (Map<String, dynamic> message) {
},
);
_firebaseMessaging.getToken().then((token) {
saveToken(token);
});
Do I have to implement some kind of background service to keep saving the new token on my DB everytime it gets refreshed? I remember using onTokenRefresh() on Android(JAVA) to do this, but I found nothing about it in Flutter (DART).
I read somewhere that the token gets refreshed every 3600 seconds. I wonder if this is true.
No, FCM token doesn't refresh every 3600 seconds. It only refreshes when :
When user Uninstall/Reinstall the app or Clears App Data
You manually delete FCM Instance using FirebaseMessaging().deleteInstanceID()
You can listen to token refresh stream using:
FirebaseMessaging().onTokenRefresh.listen((newToken) {
// Save newToken
});
Hope it helps
You can use firebaseMessaging.onTokenRefresh to get a stream which receives an event each time a new token is received.
Here is an example of subscribing to the firebaseMessaging.onTokenRefresh stream and updating the token if the token has changed:
FirebaseMessaging().onTokenRefresh.listen((token) async {
final prefs = await SharedPreferences.getInstance();
final String firebaseTokenPrefKey = 'firebaseToken';
final String currentToken = prefs.getString(firebaseTokenPrefKey);
if (currentToken != token) {
print('token refresh: ' + token);
// add code here to do something with the updated token
await prefs.setString(firebaseTokenPrefKey, token);
}
});
You can try with this.. as per new updation
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
// Save newToken
});
After the user logs in my app logs her in again automatically every 3500 seconds.
I used a Timer like this:
void _timerPressed() {
const timeout = const Duration(seconds: 3500);
new Timer.periodic(timeout, (Timer t) => _handleSignIn());
}
I set the timer in the 'login' button press method after the login has occurred:
void _loginPressed() {
print('The user wants to login with $_email and $_password');
_handleSignIn()
.then((FirebaseUser user) => print(user))
.catchError((e) => print(e));
_timerPressed();
}
(Don't be fooled by the name of the method, '_timerPressed'. I used a button press for testing the technique and haven't gotten around to renaming the method after I tied it in to the login button.)

Resources