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.
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(...)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With