skip to Main Content

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.

  1. Try to use the Nav key.
    I have a rootNavigatorKey but when I try to access it via main.rootNavigatorKey or via passing it thought methods. The value of rootNavigatorKey is null.

  2. 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 to pop 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 1 pop and its perfect. (I assume there could be some issues with initNotif 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.

  3. 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


  1. 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:

    • Null Navigator Key: The navigator key isn’t initialized yet, causing navigation attempts to fail.
    • Glitches in Routing: Rapid or multiple notifications can trigger multiple navigation events, resulting in erratic behavior like continuous routing or requiring multiple pops to reach the desired page.

    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:

    1. Ensures Navigator Readiness:
      • Delays the navigation until the widget tree is fully built, ensuring that the navigator key is properly initialized and available for use.
    2. Prevents Navigation Glitches:
      • Avoids attempting to navigate multiple times simultaneously, which can cause the app to enter unexpected states or navigate incorrectly.
    3. Stabilizes Navigation Flow:
      • Guarantees that navigation actions occur in a stable environment, reducing the likelihood of errors and improving the overall user experience.

    Implementation Steps:

    1. Delay Navigation:
      • Wrap your navigation logic inside addPostFrameCallback to ensure it runs after the widget tree is ready.
    2. Use a Global Navigator Key:
      • Assign a global navigator key to your MaterialApp or MaterialApp.router to facilitate navigation from anywhere in your app, including background handlers.
    3. Initialize Notifications Early:
      • Initialize your notification handlers before the app runs to ensure that the navigator key is set up and ready to handle navigation requests immediately.

    Summary:

    • Add addPostFrameCallback: This ensures that navigation occurs only after the widget tree is fully built, preventing null navigator issues and routing glitches.
    • Use a Global Navigator Key: Facilitates reliable navigation from any part of the app, including background processes.
    • Initialize Notifications Early: Sets up the necessary components in the correct order, ensuring that navigation can be handled smoothly when a notification is tapped.

    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.

    Login or Signup to reply.
  2. 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:

    final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
    
    GoRouter getRouter() {
      return GoRouter(
        navigatorKey: rootNavigatorKey,
        initialLocation: '/home',
        routes: [
          GoRoute(
            path: '/home',
            builder: (context, state) => HomePage(),
          ),
          GoRoute(
            path: '/details/:id',
            builder: (context, state) => DetailsPage(id: state.params['id']),
          ),
        ],
      );
    }
    

    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.

    class FirebaseNotificationAPI {
      final FirebaseMessaging fm = FirebaseMessaging.instance;
      final FlutterLocalNotificationsPlugin localNotification = FlutterLocalNotificationsPlugin();
    
      Future<void> initNotif(BuildContext context) async {
        await fm.requestPermission();
        await fm.getToken();
        await initPushNotif(context);
        await initLocalNotif(context);
      }
    
      Future<void> initPushNotif(BuildContext context) async {
        FirebaseMessaging.onMessage.listen((message) {
          if (message.notification != null) {
            localNotification.show(
              message.hashCode,
              message.notification!.title,
              message.notification!.body,
              NotificationDetails(
                android: AndroidNotificationDetails(
                  'high_importance_channel',
                  'High Importance Notifications',
                  importance: Importance.max,
                  icon: 'app_icon',
                ),
              ),
            );
          }
        });
    
        FirebaseMessaging.onMessageOpenedApp.listen((message) {
          handleMessage(context, message);
        });
    
        FirebaseMessaging.onBackgroundMessage(handler);
      }
    
      Future<void> handleMessage(BuildContext context, RemoteMessage message) async {
        if (message == null) return;
        final deepLink = message.data["DeepLink"].toString();
        await GoRouter.of(context).push(deepLink);
      }
    
      Future<void> initLocalNotif(BuildContext context) async {
        final androidSettings = AndroidInitializationSettings('@drawable/notification_icon');
        final settings = InitializationSettings(android: androidSettings);
        await localNotification.initialize(settings, onDidReceiveNotificationResponse: (details) async {
          final deepLink = jsonDecode(details.payload!)["data"]["DeepLink"];
          await GoRouter.of(context).push(deepLink);
        });
      }
    }
    

    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:

    await GoRouter.of(context).push(deepLink);
    

    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:

    FirebaseMessaging.onMessage.listen((message) {
      final notification = message.notification;
      if (notification == null) return;
      localNotification.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            'high_importance_channel',
            'High Importance Notifications',
            importance: Importance.max,
            icon: 'app_icon',
          ),
        ),
        payload: jsonEncode(message.toMap()),
      );
    });
    

    When the user taps the notification, you handle it like this:

    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      final deepLink = message.data["DeepLink"].toString();
      await GoRouter.of(context).push(deepLink);
    });
    

    I think this will solve your problem

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search