I'm trying to use FCM messaging and keep getting this error.
E/FlutterFcmService( 3684): Fatal: failed to find callback
Below is the code I've used to setup.
static Future<void> messagePiper(
Map<String, dynamic> message,
FilteredMap<String, ChatMessage> globalChatEntryMap,
FilteredMap<String, ChatMessage> gameChatEntryMap,
Subject<List<ChatMessage>> globalChatSubject,
Subject<List<ChatMessage>> gameChatSubject,
Map<String, Player> _playerMap) async {
final Map<String, dynamic> data = message['data'];
if (data.containsKey('name')) {
final msg = ChatMessage.fromMap(data);
globalChatEntryMap.putIfAbsent(msg.id, () => msg);
globalChatSubject.add(globalChatEntryMap.values.toList());
} else {
final msg = GameChatMessage.fromMap(data);
final chat = ChatMessage.fromGlobalChatMessage(
msg,
_playerMap[msg.pId].name,
_playerMap[msg.pId].imageUrl,
);
print('chat: $chat');
gameChatEntryMap.putIfAbsent(msg.id, () => chat);
print('_gameChatEntryMap : $gameChatEntryMap');
gameChatSubject.add(gameChatEntryMap.values.toList());
}
return Future<void>.value();
}
is the callback passed in to FirebaseMessaging configuration.
final FirebaseMessaging _fm = FirebaseMessaging();
#override
void initState() {
_fm.configure(
onMessage: (Map<String, dynamic> message) async {
print('onMessagee : $message');
return Utils.messagePiper(
message,
_globalChatEntryMap,
_gameChatEntryMap,
_globalChatSubject,
_gameChatSubject,
_playerMap);
},
onLaunch: (Map<String, dynamic> message) async {
print('onLaunch : $message');
return Utils.messagePiper(
message,
_globalChatEntryMap,
_gameChatEntryMap,
_globalChatSubject,
_gameChatSubject,
_playerMap);
;
},
onResume: (Map<String, dynamic> message) async {
print('onResume : $message');
return Utils.messagePiper(
message,
_globalChatEntryMap,
_gameChatEntryMap,
_globalChatSubject,
_gameChatSubject,
_playerMap);
;
},
onBackgroundMessage: null);
....
Java configuration file
package io.flutter.plugins;
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
#Override
public void onCreate() {
super.onCreate();
FlutterFirebaseMessagingService.setPluginRegistrant(this);
}
#Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}
dependency versions
random_string: 0.0.2
firebase_auth: ^0.14.0+5
firebase_database: ^3.0.7
provider: 3.0.0
rxdart: ^0.22.2
collection: ^1.14.11
audioplayers: ^0.13.2
firebase_admob: ^0.5.5
connectivity: ^0.4.4
firebase_messaging: ^5.1.6 # tried with several different versions
I tried with several firebase_messaging versions but couldn't find a fix.
Appreciate any help to solve this issue.
This error message is coming from startBackgroundIsolate which is used for allowing handling background messages.
If you don't want to handle background messages then you can safely ignore this error message. Otherwise, you need to set up a callback for handling background messages as described here
If your callback is not executed when clicking on the notification then it's because you didn't set click_action property of the message to FLUTTER_NOTIFICATION_CLICK
Are you sending FCM using the web, not FCM console?
make sure the post request is correct on your backend. I'm using Laravel
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization'=> 'key='. $token,
])->post($url, [
'notification' => [
'body' => $request->summary,
'title' => $request->title,
'image' => request()->getHttpHost().$path,
],
'priority'=> 'high',
'data' => [
'click_action'=> 'FLUTTER_NOTIFICATION_CLICK',
'status'=> 'done',
],
'to' => '/topics/all'
]);
You should declare a backgroundMessageHandler function that is outside a class or as a static function, in order to be reached from outside, and then you pass this function to fbm.configure:
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
print('on background $message');
}
fbm.configure(
onMessage: (msg) {
print(msg);
return;
},
onLaunch: (msg) {
print(msg);
return;
},
onResume: (msg) {
print(msg);
return;
},
onBackgroundMessage: myBackgroundMessageHandler
);
Also open your_project_folder/android/app/source/AndroidManifest.xml and paste this XML code after existing intent-filter code of your main Activity:
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
The result will be similar to the following code:
<activity android:name=".MainActivity" android:launchMode="singleTop" android:theme="#style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
In Application class use the below code
previously it was GeneratedPluginRegistrant.registerWith(registry);,
replace it with
FirebaseMessagingPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"));
import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin;
import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService;
public class Application extends FlutterApplication implements PluginRegistrantCallback {
#Override
public void onCreate(){
super.onCreate();
FlutterFirebaseMessagingService.setPluginRegistrant(this);
}
#Override
public void registerWith(PluginRegistry registry){
FirebaseMessagingPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"));
}
}
When I was trying to send a notification from the firebase console just to test my app receives notifications, I also encountered the same error. Make sure you kill the app on your emulator so it is not running in the background. And now try sending a notification from the firebase console, wait for a couple of seconds and you should see it. This worked for me.
Related
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
...);
I am building a home automation project that has a fire sensor that will write to Firebase Database if there is a fire detected, then from that point I need to make an alarm for the user.
I managed to trigger a notification from Firebase cloud functions, but that's not exactly what I want.
What I want is to make a full-screen notification to the user with a custom sound something like a phone alarm or a what's app call when there is a fire alarm -change in the database-.
I tried as a Top level function with no error while running my application:
firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print('onMessage: $message');
toast3('asdasdsawwwww $message');
setMessage(message);
},
onLaunch: (Map<String, dynamic> message) async {
print('onLaunch: $message');
setMessage(message);
},
onResume: (Map<String, dynamic> message) async {
print('onResume: $message');
setMessage(message);
},
onBackgroundMessage: myBackgroundMessageHandler);
print('onMessage:12qew11');
firebaseMessaging.requestNotificationPermissions(
const IosNotificationSettings(sound: true, badge: true, alert: true),
);
}
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
print('HEREE');
final assetsAudioPlayer = AssetsAudioPlayer();
assetsAudioPlayer.open(
Audio("assets/audio/alarm.mp3"),
);
return Fluttertoast.showToast(
msg: 'done background:))))$message',
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
timeInSecForIos: 4,
backgroundColor: Colors.redAccent,
textColor: Colors.white,
fontSize: 15.0);
}
My Firebase function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().functions);
var fireDatabase;
exports.myFirstCloudFun = functions.database.ref('/usersData/{userID}/Fire').onUpdate(async (event, context) => {
const uidGotten = context.params.userID;
const fireData = event.after.val()
console.log('data changed in fire is' + fireData + 'userID is ' + uidGotten);
const usereIdTokens = await admin
.firestore()
.collection(uidGotten)
.doc('userTokens')
.get();
console.log('Tokens to try are' + usereIdTokens.data);
var tokens = usereIdTokens.data().user_all_tokens;
var payload = {
notification: {
title: 'Push Title',
body: 'Push Body' + fireData,
sound: 'default',
},
data: {
push_key: 'Fire Value Is',
key1: "fireData is " + fireData,
},
};
tokens.forEach.toString().trim;
console.log('Tokens to send are ' + tokens[1] + ' ////// ' + tokens);
try {
const response = await admin.messaging().sendToDevice(tokens, payload);
console.log('Notification sent successfully');
} catch (err) {
console.log(err);
}
});
My Application.kt
package com.eghubs.eg_home_hubs
import io.flutter.app.FlutterApplication
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback
import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService
public class Application: FlutterApplication(), PluginRegistrantCallback {
override fun onCreate() {
super.onCreate()
FlutterFirebaseMessagingService.setPluginRegistrant(this)
}
override fun registerWith(registry: PluginRegistry) {
FirebaseCloudMessagingPluginRegistrant.registerWith(registry)
}
}
My FirebaseCloudMessagingPluginRegistrant.kt
package com.eghubs.eg_home_hubs
import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin
class FirebaseCloudMessagingPluginRegistrant {
companion object {
fun registerWith(registry: PluginRegistry) {
if (alreadyRegisteredWith(registry)) {
return;
}
FirebaseMessagingPlugin.registerWith(registry.registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin"))
}
fun alreadyRegisteredWith(registry: PluginRegistry): Boolean {
val key = FirebaseCloudMessagingPluginRegistrant::class.java.name
if (registry.hasPlugin(key)) {
return true
}
registry.registrarFor(key)
return false
}
}
}
My AndroidManifest is :
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.eghubs.eg_home_hubs">
<uses-permission android:name="android.permission.ACCESS_CORSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET"/>
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here.
android:name="io.flutter.app.FlutterApplication"
android:name="androidx.multidex.MultiDexApplication"
-->
<application
android:name=".Application" <!-- here is the change-->
android:label="EG HomeHubs"
android:allowBackup="false"
android:icon="#mipmap/ic_launcher">
tools:replace="android:allowBackup">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="#style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:showWhenLocked="true"
android:turnScreenOn="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="#style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="#drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter> <!-- Noti:this is for cloud messiging -->
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
but I get nothing when the application is not opened in the background, why onBackgroundMessage function is not called?
My question is how can I achieve that what's an app call or something of that kind?
OR Is there any better way to do this, some other way to achieve that fire alarm functionality in my project from the firebase database change?
EDIT: My app/build.gradle
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
//GradleException
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
packagingOptions {
exclude 'META-INF/services/javax.annotation.processing.Processor'
}
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.eghubs.eg_home_hubs"
minSdkVersion 21
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
// multiDexEnabled true
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
debug {
minifyEnabled true
shrinkResources true
}
}
}
flutter {
source '../..'
}
dependencies {
implementation 'com.google.firebase:firebase-analytics'
implementation platform('com.google.firebase:firebase-bom:26.0.0')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// implementation 'androidx.multidex:multidex:2.0.1' //with androidx libraries
implementation'com.google.firebase:firebase-messaging:21.0.1'
}
apply plugin: 'com.android.application'
// Add this line
apply plugin: 'com.google.gms.google-services'
My android/build.gradle:
buildscript {
ext.kotlin_version = '1.3.50'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.google.gms:google-services:4.3.4'
//classpath 'com.android.tools.build:gradle:3.5.3' //todo rollback this
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
I have some solutions for solving this problem.
Solution 1:
The first thing you have to do is to check if onBackgroundMessage is supported in iOS or not.
And in Android, you have to turn on the Allow running in background option.
Solution 2:
Create a high importance channel through the flutter_local_notifications package.
flutter_local_notifications package link: https://pub.dev/packages/flutter_local_notifications
Solution 3:
Create a new file App.java inside folder java/com/yourdomain
package com.yourdomain;
import io.flutter.app.FlutterApplication;
public class App extends FlutterApplication {
#Override
public void onCreate() {
super.onCreate();
}
}
Then, inside AndroidManifest.xml file, add android:name=".App"
<application
android:name=".App"
...
>
After that rebuild the application and the notifications will work correctly even when the app is terminated or killed.
On Android, for your onBackgroundMessage to be called when the app is in the background the FCM message must be a data message without notification, look here: https://firebase.google.com/docs/cloud-messaging/android/receive
Also when the device is sleeping, to receive the messages without much delay you should disable the battery optimization, look at this: https://developer.android.com/training/monitoring-device-state/doze-standby
Alright.
Since update 8.0.0-dev.1
we can get onBackgroundMessage by default without any need for these installation process
NEW: FirebaseMessaging.onBackgroundMessage() Sets a background message
handler to trigger when the app is in the background or terminated.
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
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.
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