Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct place to init notifications in Flutter

I am currently struggling with getting notifications run the way I want them to on Flutter. My current solution is to have a dynamic LandingPage, which - depending on the FirebaseAuth - redirected to either the Login- or the Main-Screen.

MaterialApp(
   theme: ThemeData(
   ...
   home: Scaffold(body: Builder(builder: (context) => LandingPage()))
),

Inside the LandingPage, I will call a function in my Singleton to do the setup of the notifications for me. As you can see, I am passing the context here. This is, because I want to show Snackbar from the onMessage callback of my notifications.

class LandingPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    FirebaseUser user = Provider.of<FirebaseUser>(context);
    if (user != null) {
      Singleton().setupMessaging(user.uid, context); //This is the line
      return MainPage(userId: user.uid);
    } else {
      while(Navigator.canPop(context)){
        Navigator.pop(context);
      }
      return LoginPage();
    }
  }
}

By that I am trying to achieve, to get the messaging system running once a user is logged in. I only setup the callbacks if the current token is undefined.

The problem I got now is, that said context is not application-wide, meaning, once I navigate to a new Widget, which has his own context, the Snackbar can no longer be shown. Now I am unsure, if this is the correct place to initialize the messaging, since there is no application-wide context.

setupMessaging(String uid) async{
   if((await SharedPreferences.getInstance()).getBool('bNotifications') ?? true){
     print("bNotifications disabled");
     return;
   }

   _firebaseMessaging.getToken().then((token) {
     if (_lastToken != token) {
       if (_lastToken == null) {
         if (Platform.isIOS) iOSPermission();
         _firebaseMessaging.configure(
           onMessage: (Map<String, dynamic> message) async {
             print('onMessage $message');
             Scaffold.of(context).showSnackBar(...); //Here I need the context
           },
           onResume: (Map<String, dynamic> message) async {
             print('onResume $message');
           },
           onLaunch: (Map<String, dynamic> message) async {
             print('onLaunch $message');
           },
         );
       }
       _lastToken = token;
     }
   });
}

I also considered showing a local notification inside the onMessage-callback, but local notifications and firebase-messaging do not work together on iOS.

The last option I heard about is using a GlobalKey, which I would need to pass through all my pages. This approach is also very slow as I heard.

like image 801
Thomas Avatar asked May 22 '19 10:05

Thomas


2 Answers

There is no such thing as an application-wide Context, but you could create an application-wide Scaffold for your Snackbars.

Initialize a GlobalKey that can be accessed statically. It must be initialized before any route is built, eg. in your application's main() function, or in the state of the widget that returns the MaterialApp:

static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();

Add a builder to your MaterialApp which wraps each page with a Scaffold:

builder: (BuildContext context, Widget child) {
  return Scaffold(
    key: scaffoldKey,
    body: child,
  );
}

Now you no longer need a page-specific Context as you can use the top-level Scaffold to display Snackbars:

MyAppState.scaffoldKey.currentState.showSnackBar(...)
like image 184
Ovidiu Avatar answered Sep 28 '22 05:09

Ovidiu


One way to do this is with streams (you don't have to be using BLoC to love streams!)

For example, in your app state, you could have something like these members:

StreamController<NotificationData> _notificationsController;
Stream<NotificationData> get notificiations => _notificationsController.stream;
void sendNotification(NotificationData n) {
  _notificationsController.add(n);
}

This is really neat because you can call sendNotification from other parts of the app, or in your business logic, or wherever is appropriate.
Now, you make a new widget wherever you want the notification to display. This is what I use in my app (it's for dialogs though):

class DialogListener extends StatefulWidget {
  final Stream stream;
  final WidgetBuilder dialogBuilder;
  final Widget child;
  DialogListener({Key key, this.stream, this.dialogBuilder, this.child}) : super(key: key);

  @override
  _DialogListenerState createState() => new _DialogListenerState();
}

class _DialogListenerState extends State<DialogListener> {
StreamSubscription _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = widget.stream?.listen((_) {
      if (mounted) {
        showDialog(context: context, builder: widget.dialogBuilder);
      }
    });
  }

  @override
  void dispose() { 
    _subscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

Then I can just wrap my page or whatever in DialogListener

DialogListener(
  stream: Provider.of<MyState>(context).notifications,
  dialogBuilder: (context) => MyNotificationDialog(),
  child: RestOfMyPage()
)

Notice in my implementation, I don't actually use any data from the stream so you'd have to add that in.

It's tempting to pass context to your state, but as you've found it's a bad idea for various reasons (particularly unit testing) and means you should probably rethink the architecture or do something like this.

like image 44
Lucas Avatar answered Sep 28 '22 06:09

Lucas