3

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!

F.SO7
  • 695
  • 1
  • 13
  • 37
  • Can you show the code for the `NotificationHandler` class? – Andrej Mar 08 '21 at 15:06
  • @Andrej I have added it to my question – F.SO7 Mar 08 '21 at 15:06
  • Can you also show the code for sending notifications? – Andrej Mar 08 '21 at 15:07
  • @Andrej Yes, I did add it now to my question. Please note the notifications are being sent through an external React admin panel, and not through the Flutter app – F.SO7 Mar 08 '21 at 15:12
  • In your _fcm.configure() method did you missed "onBackgroundMessage" parameter ? -> onBackgroundMessage: yourBackgroundMessageHandler , where "yourBackgroundMessageHandler" should be your top level function?. And talking about iOS , have you created APNS certificate and properly linked with your GoogleServiceInfo.plist file? – Tanmay Pandharipande Mar 11 '21 at 10:14

4 Answers4

1

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));
    }
  });
}
Stewie Griffin
  • 4,690
  • 23
  • 42
  • getInitialMessage can be accessed only in the newest version of firebase. Unfortunately, I am using the previous version – F.SO7 Mar 09 '21 at 14:12
  • I see. Since I'm unable to check the rest of your code, I can only speculate to spot the problem. Did you call requestNotificationPermissions before registering the handlers? – Stewie Griffin Mar 09 '21 at 21:45
0

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.

Kirill Bubochkin
  • 5,868
  • 2
  • 31
  • 50
  • Thank you for your answer. I am testing my iOS app using a real device via TestFlight and it doesn't work. Regarding the Android version - I try to achieve that when the user clicks on a notification it will be launched again but will present the correct screen of the notification and not the main screen of the app – F.SO7 Mar 08 '21 at 15:19
  • @F.SO7 do you request permissions on iOS? – Kirill Bubochkin Mar 08 '21 at 15:33
  • Yes, I do receive the notifications themselves completely fine on both Android and iOS. But when the user clicks on the notification it opens the main screen and not the screen I try to open (the user's profile, for example) – F.SO7 Mar 08 '21 at 15:34
  • Oh, I see. Then the problem is actually in WHEN you register the notification handler. You're registering it in MainScreen – it's too late, as by this time notification is already processed. You should register handler sooner, e.g. in main function. – Kirill Bubochkin Mar 08 '21 at 16:19
  • But in the main function I cannot navigate to a specific screen. How can I navigate to the relevant screen on the main function – F.SO7 Mar 08 '21 at 17:01
  • This is a different question and has many possible solutions. But the general idea is to save desired route information and later retrieve it and process (e.g. with providers/blocs/inherited widgets etc). – Kirill Bubochkin Mar 08 '21 at 17:07
  • Can you please explain how can I achieve that using Provider? – F.SO7 Mar 09 '21 at 07:10
0

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.

hyobbb
  • 332
  • 1
  • 9
  • Is there a way to implement it not using get_it? I am using Provider – F.SO7 Mar 08 '21 at 15:38
  • Normally Provider use context for accessing instance. If you are using Riverpod it is possible. – hyobbb Mar 08 '21 at 15:40
  • Can you please explain more about implementing it using Provider? – F.SO7 Mar 08 '21 at 15:47
  • https://stackoverflow.com/questions/12649573/how-do-you-build-a-singleton-in-dart this is how to make singleton instance in dart. If you make your configuration provider instance before initializing App you can access to it's instance. But I never tried that way. – hyobbb Mar 08 '21 at 15:51
  • I cannot understand how it is relevant to the question – F.SO7 Mar 08 '21 at 15:52
  • Because you have to manage something before you actually build your App where you inject your providers. If you don't want to use service locator use any global variable to handle initial configuration for your app. – hyobbb Mar 17 '21 at 07:52
0

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
...);
kabayaba
  • 195
  • 1
  • 13