Reputation: 144
My app consists of a StatefulWidget
at the root of the tree that passes its state along with callbacks to change the state to an InheritedWidget
. Inside one of these callbacks a listener for a document inside Firebase's Firestore is created to listen for changes and then display a SnackBar
informing the user about the change.
The problem now is how to access the currently active Scaffold
from the parent at the root of the tree. Between the root and the currently active Scaffold
could be multiple other Scaffolds
depending on how many routes were pushed onto the Navigator
. But to display a SnackBar
the most recent one must be used.
Upvotes: 2
Views: 3141
Reputation: 186
TLDR:
Use ModalRoute.of(context).isCurrent
to determine if the current context is visible and pass the scaffolkey in the widget's build method.
Details
My application is slightly different but I believe my solution is still applicable.
In my application I have several Providers that run across different pages but may all need to raise a snackbar. Hence the provider needs to know which scaffold is active.
I have discovered and made use of ModalRoute.of(context).isCurrent
which allows you to determine if the current context is visible.
I created the following mixin:
import 'package:flutter/material.dart';
mixin SnackBarHandler {
GlobalKey<ScaffoldState> _scaffoldKey;
/// Sets the scaffold key if the caller is active.
void setSnackBarContext(
BuildContext context, GlobalKey<ScaffoldState> scaffoldKey) {
if (ModalRoute.of(context).isCurrent) {
_scaffoldKey = scaffoldKey;
}
}
/// Convenience wrapper to show snack bar.
void showSnackbar(SnackBar snackBar) {
_scaffoldKey?.currentState?.showSnackBar(snackBar);
}
}
Which is added to any class you need to show snack bars from as follows:
class BleDeviceProvider extends ChangeNotifier with SnackBarHandler{
showSnackbar(SnackBar());
}
Each page that you want to show snack bars from the call above must include the following in their build:
Widget build(BuildContext context) {
return Consumer<BleDeviceArc003Provider>(
builder: (_, bleDeviceArc003Provider, __) {
bleDeviceArc003Provider.setSnackBarContext(context, _scaffoldKey);
return WillPopScope(
onWillPop: _onPop,
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(),
body: ListView()
),
}
});
}
Upvotes: 1
Reputation: 144
My current solution can be found in the following code sample. It works but I'm all open for better ideas. What I currently dislike about this solution is that I need to call the callback to push the current GlobalKey
from within didChangeDependencies
and therefore introduce a member variable to make sure this is only called once in the lifetime of the Widget
because the InheritedWidget
cannot be accessed from initState
.
Does anybody have an alternative to having to introduce the member variable?
class MyApp extends StatefulWidget {
final Firestore firestore;
const MyApp(this.firestore);
@override
State<StatefulWidget> createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
void addDocumentListener(DocumentReference ref) {
ref.snapshots.listen((snapshot) {
_scaffoldKeys.last.currentState.showSnackBar(new SnackBar(content: new Text("Test")));
});
}
var _scaffoldKeys = new Queue<GlobalKey<ScaffoldState>>();
void pushScaffoldKey(GlobalKey<ScaffoldState> key) {
_scaffoldKeys.addLast(key);
}
GlobalKey<ScaffoldState> popScaffoldKey() {
return _scaffoldKeys.removeLast();
}
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(addDocumentListener, pushScaffoldKey, popScaffoldKey, new MaterialApp(home: new HomePage()));
}
}
typedef VoidDocumentReferenceCallback(DocumentReference ref);
typedef VoidGlobalKeyScaffoldStateCallback(GlobalKey<ScaffoldState> key);
typedef GlobalKey<ScaffoldState> GlobalKeyScaffoldStateCallback();
class MyInheritedWidget extends InheritedWidget {
final VoidDocumentReferenceCallback addDocumentListener;
final VoidGlobalKeyScaffoldStateCallback pushScaffoldKey;
final GlobalKeyScaffoldStateCallback popScaffoldKey;
const MyInheritedWidget(this.addDocumentListener, this.pushScaffoldKey, this.popScaffoldKey, Widget child) : super(child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new CustomScaffold(
appBar: new AppBar(
title: new Text("Home Page"),
actions: <Widget>[
new IconButton(icon: const Icon(Icons.settings), onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
// go to another page with a CustomScaffold
}));
})
],
),
body: new Center(
child: new RaisedButton(onPressed: () {
MyInheritedWidget.of(context).addDocumentListener(ref);
}, child: new Text("add listener")),
),
);
}
}
class CustomScaffold extends StatefulWidget {
final AppBar appBar;
final Widget body;
const CustomScaffold({this.appBar, this.body});
@override
State<StatefulWidget> createState() {
return new CustomScaffoldState();
}
}
class CustomScaffoldState extends State<CustomScaffold> {
final _key = new GlobalKey<ScaffoldState>();
bool _keyInitialized = false;
@override
didChangeDependencies() {
if (!_keyInitialized) {
MyInheritedWidget.of(context).pushScaffoldKey(_key);
_keyInitialized = true;
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
var scaffold = new Scaffold(
key: _key,
appBar: widget.appBar,
body: widget.body,
);
return new WillPopScope(child: scaffold, onWillPop: () {
MyInheritedWidget.of(context).popScaffoldKey();
return new Future.value(true);
});
}
}
Upvotes: 1