for the last few days Ive been trying to set up such that the firebase FCM notifications I get can reroute users to the correct page that is set in the payload. I have tried a few things but have failed.
-
Try to use the Nav key.
I have arootNavigatorKey
but when I try to access it viamain.rootNavigatorKey
or via passing it thought methods. The value ofrootNavigatorKey
is null. -
Try to use context.
I pass in context but sometime it glitches. meaning that it will continously will route to page X. so when I try topop
on said page X, it leads me to page X. I may have to pop a few times for me to go to the actual page that i want to.[log] Notification received: New Employee 12 [log] Notification received: New Employee 13 [log] Notification received: New Employee 14 [log] Notification received: New Employee 15 [log] Notification received: New Employee 16
The intresting thing here is some time even with the slew of messages (
Notification received
), I at times just do 1pop
and its perfect. (I assume there could be some issues withinitNotif
when nav between pages). P.S. I tried to init in the main but then context come up as null.I tried to use a MUTEX as well as a deboucer around the
context.push
but neither worked, code ran thru the checks. -
Try to use
addPostFrameCallback
This was promising but after finding the issuse above (#2). This would cause delays in pops and thus more issues.
I am using go_router: 12.1.3
due to PopScope Issue on Android
This is my notification class:
const appLinkLength = "xxxxxx".length;
Future<void> handler(RemoteMessage message) async {
developer.log("Handling background message: ${message.messageId}");
}
class FirebaseNotificationAPI {
final FirebaseMessaging fm = FirebaseMessaging.instance;
final AndroidNotificationChannel androidChannel =
const AndroidNotificationChannel(
"high_importance_channel",
"High Importance Notifications",
description: "Notification that you must see",
importance: Importance.max,
);
final FlutterLocalNotificationsPlugin localNotification =
FlutterLocalNotificationsPlugin();
Future<void> initNotif(BuildContext context) async {
// Request permissions for iOS/Android.
await fm.requestPermission();
final token = await fm.getToken();
developer.log("FCM Token: $token");
// Initialize local and push notifications.
await initPushNotif(context);
await initLocalNotif(context);
}
Future<void> handleMessage(
BuildContext context, RemoteMessage? message) async {
if (message == null) return;
developer.log("Notification received: ${message.notification?.title}");
final deepLink =
message.data["DeepLink"].toString().substring(appLinkLength);
await GoRouter.of(context).push(deepLink);
}
Future<void> initLocalNotif(BuildContext context) async {
const androidSettings =
AndroidInitializationSettings('@drawable/notification_icon');
const settings = InitializationSettings(android: androidSettings);
await localNotification.initialize(
settings,
onDidReceiveNotificationResponse: (details) async {
developer.log("Notification response details: $details");
final deepLink = jsonDecode(details.payload!)["data"]!["DeepLink"]
.toString()
.substring(appLinkLength);
();
await GoRouter.of(context).push(deepLink);
},
);
// Create the notification channel on Android.
final platform = localNotification.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (platform != null) {
await platform.createNotificationChannel(androidChannel);
}
}
Future<void> initPushNotif(BuildContext context) async {
// Handle the initial notification if the app is opened via a notification.
fm.getInitialMessage().then(
(message) {
handleMessage(context, message);
},
);
// Handle messages when the app is in the foreground.
FirebaseMessaging.onMessage.listen((message) {
final notification = message.notification;
if (notification == null) return;
developer.log("Foreground notification: ${notification.title}");
localNotification.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
androidChannel.id,
androidChannel.name,
channelDescription: androidChannel.description,
icon: '@drawable/notification_icon',
),
),
payload: jsonEncode(message.toMap()),
);
});
// Handle messages when the app is reopened via a notification.
FirebaseMessaging.onMessageOpenedApp.listen((message) {
handleMessage(context, message);
});
// Handle background messages.
FirebaseMessaging.onBackgroundMessage(handler);
}
}
this is my main
final log = Logger('JumpLogger');
final rootNavigatorKey = GlobalKey<NavigatorState>();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//to load production env variables replace '.env.dev' with '.env.prod'
.....
runApp(MyApp(sellerOrConsumerActive: sellerOrConsumerActive));
}
Widget getHomePage(String sellerOrConsumerActive) {
....
}
}
Future<String> getBuildCert() async {
const platform = MethodChannel('com.mazaar.frontend/keys');
try {
final String buildCert = await platform.invokeMethod('getBuildCert');
return buildCert;
} catch (e) {
developer.log("Failed to get certs: $e");
return "";
}
}
Future<void> fetchAndActivateConfig() async {
final remoteConfig = FirebaseRemoteConfig.instance;
await remoteConfig.setConfigSettings(RemoteConfigSettings(
fetchTimeout: const Duration(minutes: 1),
minimumFetchInterval: Duration(
seconds: int.parse(dotenv.env['FIREBASE_REMOTE_CONFIG_DURATION']!)),
));
try {
// Fetch and activate
await remoteConfig.fetchAndActivate();
} catch (e) {
developer.log('Failed to fetch remote config: $e');
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.sellerOrConsumerActive});
final String sellerOrConsumerActive;
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: getRouter(sellerOrConsumerActive),
theme: CustomTheme.themeData,
debugShowCheckedModeBanner: false,
);
}
}
GoRouter getRouter(sellerOrConsumerActive) {
final Widget homePage = getHomePage(Environment.getSellerOrConsumerActive());
final shellNavigatorKey = GlobalKey<NavigatorState>();
return GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: RoutingConstants.root.path,
routes: <RouteBase>[
ShellRoute(
navigatorKey: shellNavigatorKey,
routes: [
GoRoute(
name: RoutingConstants.address.name,
path: RoutingConstants.address.path,
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
child: const ChangeAddressPage(),
),
),
GoRoute(
name: RoutingConstants.root.name,
path: RoutingConstants.root.path,
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
child: homePage,
),
routes: [
GoRoute(
name: RoutingConstants.item.name,
path: RoutingConstants.item.subroute,
pageBuilder: (context, state) => NoTransitionPage<void>(
key: state.pageKey,
child: ItemPage(
itemId: state.pathParameters['itemId'].toString(),
),
),
),
...
],
builder: (context, state, child) {
return child;
},
)
],
);
}
this is where I call FirebaseNotificaitionAPI.initNotif(...)
class ProfilePage extends StatelessWidget {
...
Future<Map<String, dynamic>> getProfileData(BuildContext context) async {
await FirebaseNotificationAPI().initNotif(context);
final user = firebaseAuth.currentUser;
final storeExists = await checkIfStoreExists(context);
return {
'user': user,
'storeExists': storeExists,
};
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false || Environment.isSellerAndConsumer(),
child: ScaffoldWrapper(
showBack: false || Environment.isSellerAndConsumer(),
child: FutureBuilder<Map<String, dynamic>>(
future: getProfileData(context),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return const ErrorDisplay(
errorMessage: 'Error could not load profile data.',
);
} else if (!snapshot.hasData) {
return const ErrorDisplay(
errorMessage: 'No data available.',
);
} else {
final user = snapshot.data!['user'] as User?;
final storeExists = snapshot.data!['storeExists'] as bool;
final profilePictureUrl = user?.photoURL ?? image;
return Column(
children: [
Environment.isSellerAndConsumer()
? const HeaderSearchBar(
initText: '',
)
: Container(),
Padding(
padding: const EdgeInsetsDirectional.fromSTEB(10, 0, 10, 0),
child: ListView(
...
children: [
profilePicture(context, profilePictureUrl),
welcomeTitle(user),
storeExists
? seeYourStoreButton(context)
: businessButton(context),
signOutButton(context),
manageDataButton(context),
],
),
),
],
);
}
},
),
),
);
}
...
and this one as well
@override
void initState() {
super.initState();
_firebaseInit = FirebaseNotificationAPI().initNotif(context);
}
@override
Widget build(BuildContext context) {
final Uri topBannerAdUrl = Uri.parse('https://flutter.dev');
return ScaffoldWrapper(
showBack: false,
child: Column(
children: [
const HeaderSearchBar(
initText: '',
),
FutureBuilder<void>(
future: _firebaseInit,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else {
return Expanded(
child: SingleChildScrollView(
child: Column(
children: [
GestureDetector(
onTap: () => _launchUrl(topBannerAdUrl),
child: const ImageLoader(imageUri: imageUrl),
),
const Padding(padding: EdgeInsets.only(top: 10)),
Column(
children: List.generate(
itemTitles.length,
(index) {
String title = (index < itemTitles.length)
? itemTitles[index]
: 'Hot Items';
return ItemRow(title: title);
},
),
),
],
),
),
);
}
},
),
],
),
);
}
}
2
Answers
Core Issue: Missing addPostFrameCallback
When a notification is tapped, your app attempts to navigate to a specific page. However, if this navigation occurs before the Flutter widget tree is fully built, the navigator might not be ready. This leads to problems such as:
Solution: Using addPostFrameCallback
By incorporating WidgetsBinding.instance.addPostFrameCallback, you ensure that the navigation logic executes after the current frame is rendered. This approach provides several benefits:
Implementation Steps:
Summary:
By addressing the timing of your navigation logic with addPostFrameCallback and ensuring the navigator is properly initialized, you can achieve reliable and seamless navigation in response to notification taps.
If you’re trying to handle navigation when someone taps a push notification, here’s how you can do it in Flutter using GoRouter for managing your routes.
1. Managing Navigation Context:
To navigate in Flutter when you don’t have direct access to the context (like when the app is in the background), you need a
GlobalKey<NavigatorState>
. This lets you interact with the navigator from anywhere. Here’s how you can set it up:This allows you to handle navigation even when the app is in the background or you’re dealing with a push notification tap.
2. Setting Up Firebase Push Notifications:
First, you need to set up Firebase push notifications. Here’s a class to manage the push notifications, including permission requests, handling notification taps, and background notifications.
3. Handling Deep Links:
Push notifications usually contain deep links like /item/123. When a user taps a notification, you want to navigate them to the correct screen. Here’s how to handle it:
This ensures the user is directed to the proper page based on the deep link from the notification.
4. Foreground Notifications:
When your app is in the foreground, you may want to show a local notification to alert the user. Here’s how to handle that:
When the user taps the notification, you handle it like this:
I think this will solve your problem