Flutter firebase push notification not routing to specific page - firebase

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

Related

How to open my app from background and navigate to a page on receiving a notification message in flutter?

I need to open the app automatically on receiving a notification message. Is it possible in flutter?
Below is the code for handling the background messages and it works.
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print('Handling a background message ${message.messageId}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
......
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp()
)
);
What I need is I have a separate page that needs to popup when that particular function is executed (When the app is in background). Can anyone help me out with this! Thanks in advance
await _firebaseMessaging.subscribeToTopic('topic name');
_firebaseMessaging.configure
(
// The onMessage function triggers when the notification is
received while we are running the app.
onMessage: (message) async
{
setState(()
{
messageTitle = message["notification"]["title"];
messageDescription = message["notification"]["description"];
notificationAlert = "New Notification Alert";
});
},
// The onResume function triggers when we receive the notification alert in the device notification bar and opens the app through the push notification itself. In this case, the app can be running in the background or not running at all.
onResume: (message) async
{
print("ON RESUME");
setState(()
{
messageTitle = message["data"]["title"];
messageDescription = message["notification"]["description"];
notificationAlert = "Application opened from Notification";
});
},
onLaunch: (Map<String, dynamic> message) async // Called when app is terminated
{
print("onLaunch: $message");
var data = message["data"];
print(data);
// Navigator.pushNamed(context, "details");
}
);
In this code the onResume function will help you run the app from background so you can code inside onResume and navigate to your specified screen.

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
...);

Flutter fcm redirect issue

Hi in one of my flutter project, I am using firebase messaging. First a splash screen and second, the main page of the application. In second page, I implemented the firebase.configure method in the init state as follows. The _navigateToItemDetail method leads to an another page
#override
void initState() {
super.initState();
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
setState(() {
_newNotification = true;
});
},
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
_navigateToItemDetail(message);
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
_navigateToItemDetail(message);
},
);
and I cam calling the web service for this page after this. But the above method will launch after the webservice calls. So that will cause error in the page redirection. I just put a delay of 4 seconds in web service call, then it will works fine. Is there is any method to solve the issue ? async method available for firebase config ?
I think you need 'onBackgroundMessage'
firebaseMessaging.configure(
//...
onBackgroundMessage: Platform.isIOS ? null : myBackgroundMessageHandler
//...
)
//...
static Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) async {
//Do here something
}
as you can see, it only work on android

Flutter FCM onBackgroundMessage does not work for nested non-static method calls

I'm using firebase_messaging 6.0.9 with flutter 1.12.13. In the repo's readme: https://pub.dev/packages/firebase_messaging#-readme-tab- it says to declare the onBackgroudMessage callback as a static or top level method. Which I did, but it doesn't work when this callback invokes a non-static method. The following example demonstrates this with a singleton class:
class NotificationService {
static NotificationService _instance;
final FirebaseMessaging _firebase;
static NotificationService get instance => _instance;
NotificationService._internal() : this._firebase = FirebaseMessaging();
factory NotificationService() {
if (_instance == null) {
_instance = NotificationService._internal();
_instance._firebase.configure(
onBackgroundMessage: NotificationService.staticHandler
);
}
return _instance;
}
static Future<dynamic> staticHandler(Map<String, dynamic> msg) {
print("Static Func >>> $msg"); // Successfully prints
return NotificationService.instance.instanceFunc(msg); // Fails here, complaining that it's being invoked on null.
}
Future<dynamic> instanceFunc(Map<String, dynamic> msg) {
print("Instance Func >>> $msg");
}
void myVarFunc() {
print("This is my var func");
}
}
in main.dart, the notification service factory constructor is called:
import 'package:myProject/services/notification/notification_service.dart';
run(MyApp());
class MyApp extends StatelessWidget {
final NotificationService _ns = NotificationService();
NotificationService.instance.myVarFunc(); // Prints successfully.
.......
.......
.......
}
The invocation of instanceFunc fails, saying it's being called on null. The following are the logs:
I/flutter ( 6935): Static Func >>> {data: {title: Title_is_here, message: Message_is_here}}
I/flutter ( 6935): Unable to handle incoming background message.
I/flutter ( 6935): NoSuchMethodError: The method 'instanceFunc' was called on null.
I/flutter ( 6935): Receiver: null
I/flutter ( 6935): Tried calling: instanceFunc(_LinkedHashMap len:1)
I'm not really sure if this is right way to handle this scenario. Since I'm new to Dart and Flutter, my knowledge is pretty limited. I can't declare everything static and work, that's not good design IMO. I'm probably missing something here.
There are some reason of not getting callback on onBackgroundMessage:-
onBackgroundMessage didn't worked on iOS so you have to implement platform check
For example:-
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
printDebug("onMessage foreground: $message");
},
onBackgroundMessage: Platform.isIOS ? null : _myBackgroundMessageHandler,
onLaunch: (Map<String, dynamic> message) async {
printDebug("onLaunch Kill: $message");
},
onResume: (Map<String, dynamic> message) async {
print("onResume Background: $message");
},
);
static Future<dynamic> _myBackgroundMessageHandler(
Map<String, dynamic> message) async {
print("onBackgroundMessage: $message");
return Future<void>.value();
}
And You have to make sure that your Notification payload didn't contain the notification key because if notification key is exist in payload then notification directly handled by your system. So you have to remove the notification key from payload to get callback on onBackgroundMessage.
Note:- If you remove the notification key then notification didn't rendered in systems notification tray. For this you can you local notification.

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

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.

Resources