Reputation: 3517
I am trying to track current Scaffold
s (their BuildContext
s) in order to create an app-wide SnackBar
function. Currently I am creating a class
which presents a Scaffold
and adds its context
to another class
, which manages the currently running Scaffold
s. I did not succeed, however, as my current attempt has two issues:
Scaffold
sdispose
method is too late for removing the Scaffold
's BuildContext
from the List
of current Scaffold
s' BuildContext
s, so this presents me with the Exception
, "Looking up a deactivated widget's ancestor is unsafe.
"Here is my current attempt:
main.dart
):import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'MScaffold.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Snackbar manager',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Snackbar manager'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return MScaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
heroTag: 0,
child:Icon(Icons.add_circle_outline),
onPressed: (){
MScaffoldManager.showSnackbar();
},
),
FloatingActionButton(
heroTag: 1,
child:Icon(Icons.remove_circle_outline),
onPressed: (){
MScaffoldManager.hideSnackbar();
},
),
FloatingActionButton(
heroTag: 2,
child:Icon(Icons.add),
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return SecondScaffold();
}
)
);
},
),
],
),
);
}
}
class SecondScaffold extends StatelessWidget{
@override
Widget build(BuildContext context){
return MScaffold(
appBar: AppBar(
title: Text("Page 2"),
),
body: Center(),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
heroTag: 0,
child:Icon(Icons.add_circle_outline),
onPressed: (){
MScaffoldManager.showSnackbar();
},
),
FloatingActionButton(
heroTag: 1,
child:Icon(Icons.remove_circle_outline),
onPressed: (){
MScaffoldManager.hideSnackbar();
},
),
FloatingActionButton(
heroTag: 2,
child:Icon(Icons.remove),
onPressed: (){
Navigator.of(context).pop();
},
),
],
),
);
}
}
library
class
es: MScaffoldManager
; MScaffold
; and MScaffoldState
:
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class MScaffoldManager{
static List<Map> scaffoldInformation = List();
static void addScaffold(context){
scaffoldInformation.add({'context':context});
print("Scaffold added:\n"+scaffoldInformation.toString());
}
static void removeScaffold(context){
Scaffold.of(context).hideCurrentSnackBar();
scaffoldInformation.remove({'context':context});
print("Scaffold removed:\n"+scaffoldInformation.toString());
}
static void showSnackbar(){
scaffoldInformation.forEach((v){
Scaffold.of(v['context']).showSnackBar(SnackBar(
content: Text("Snackbar works"),
));
});
}
static void hideSnackbar(){
scaffoldInformation.forEach((v){
Scaffold.of(v['context']).hideCurrentSnackBar();
});
}
}
class MScaffold extends StatefulWidget{
Key key;
var appBar;
var body;
var floatingActionButton;
var floatingActionButtonLocation;
var floatingActionButtonAnimator;
var persistentFooterButtons;
var drawer;
var endDrawer;
var bottomNavigationBar;
var bottomSheet;
var backgroundColor;
var resizeToAvoidBottomPadding;
var resizeToAvoidBottomInset;
var primary;
var drawerDragStartBehavior;
var extendBody;
var extendBodyBehindAppBar;
var drawerScrimColor;
var drawerEdgeDragWidth;
MScaffold({
Key key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null);
@override
State<StatefulWidget> createState() {
return MScaffoldState(
key: key,
appBar: appBar,
body: body,
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButtonAnimator: floatingActionButtonAnimator,
persistentFooterButtons: persistentFooterButtons,
drawer: drawer,
endDrawer: endDrawer,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
backgroundColor: backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
primary: primary,
drawerDragStartBehavior: drawerDragStartBehavior,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
drawerScrimColor: drawerScrimColor,
drawerEdgeDragWidth: drawerEdgeDragWidth,
);
}
}
class MScaffoldState extends State<MScaffold> {
Key key;
var appBar;
var body;
var floatingActionButton;
var floatingActionButtonLocation;
var floatingActionButtonAnimator;
var persistentFooterButtons;
var drawer;
var endDrawer;
var bottomNavigationBar;
var bottomSheet;
var backgroundColor;
var resizeToAvoidBottomPadding;
var resizeToAvoidBottomInset;
var primary;
var drawerDragStartBehavior;
var extendBody;
var extendBodyBehindAppBar;
var drawerScrimColor;
var drawerEdgeDragWidth;
MScaffoldState({
Key key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null);
@override
void initState() {
super.initState();
}
@override
dispose(){
MScaffoldManager.removeScaffold(_scaffoldContext);
super.dispose();
}
BuildContext _scaffoldContext;
@override
Widget build(BuildContext context) {
return Scaffold(
key: key,
appBar: appBar,
body: Builder(
builder: (context){
if(_scaffoldContext!=null)
MScaffoldManager.removeScaffold(_scaffoldContext);
_scaffoldContext = context;
MScaffoldManager.addScaffold(_scaffoldContext);
return body;
},
),
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButtonAnimator: floatingActionButtonAnimator,
persistentFooterButtons: persistentFooterButtons,
drawer: drawer,
endDrawer: endDrawer,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
backgroundColor: backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
primary: primary,
drawerDragStartBehavior: drawerDragStartBehavior,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
drawerScrimColor: drawerScrimColor,
drawerEdgeDragWidth: drawerEdgeDragWidth,
);
}
}
Acknowledging that it doesn't work as it is, it also seems a little bit verbose and a bit messy. What I would like to do is simply make a Scaffold
class
that functions just like a Scaffold
, but works with a manager class
, which keeps track of all of the Scaffold
s' context
s so I can easily display SnackBar
messages, regardless of what page the user is on.
Upvotes: 0
Views: 2624
Reputation: 3517
Thanks to some help from Ovidiu, I found a good answer. I decided to forgo Provider
in favor of a static class. I think the implementation is simpler that way. Additionally, I also made it show a SnackBar
on a new page (with a new Scaffold
) if one is opened.
All you need to do to implement it, is to import
MScaffold.dart
and change your Scaffold
s to MScaffold
s.
I find this method very convenient so helpfully it can help some other people out too, who are also looking for an easy way to show a SnackBar
, regardless of which Scaffold
is currently showing.
Here is the presentation page:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'MScaffold.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Snackbar manager',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Snackbar manager'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return MScaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
heroTag: 0,
child: Icon(Icons.add_circle_outline),
onPressed: () {
ShowSnackBar().showText("You were on page 1");
},
),
FloatingActionButton(
heroTag: 1,
child: Icon(Icons.remove_circle_outline),
onPressed: () {
ShowSnackBar().hide();
},
),
FloatingActionButton(
heroTag: 2,
child: Icon(Icons.add),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return SecondScaffold();
}));
},
),
],
),
);
}
}
class SecondScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MScaffold(
appBar: AppBar(
title: Text("Page 2"),
),
body: Center(),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FloatingActionButton(
heroTag: 0,
child: Icon(Icons.add_circle_outline),
onPressed: () {
ShowSnackBar().show(
SnackBar(
content: Text("You were on page 2"),
),
);
},
),
FloatingActionButton(
heroTag: 1,
child: Icon(Icons.remove_circle_outline),
onPressed: () {
ShowSnackBar().hide();
},
),
FloatingActionButton(
heroTag: 2,
child: Icon(Icons.remove),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
);
}
}
Here is MScaffold.dart
:
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class ShowSnackBar extends ChangeNotifier {
SnackBar currentSnackBar;
int lastHideTime = -1; //in millisecondsSinceEpoch
String _msg;
bool isSnackBarVisible = false;
static final _thisClass = ShowSnackBar._internal();
ShowSnackBar._internal();
factory ShowSnackBar() {
return _thisClass;
}
ChangeNotifier showNotifier = ChangeNotifier();
ChangeNotifier hideNotifier = ChangeNotifier();
showText(String inputMsg) {
_msg = inputMsg;
currentSnackBar = SnackBar(content: Text(_msg));
isSnackBarVisible = true;
showNotifier.notifyListeners();
}
show(SnackBar inputSnackBar) {
currentSnackBar = inputSnackBar;
isSnackBarVisible = true;
showNotifier.notifyListeners();
}
hide() {
hideNotifier.notifyListeners();
}
}
class MScaffold extends Scaffold {
ValueKey key;
var appBar;
var body;
var floatingActionButton;
var floatingActionButtonLocation;
var floatingActionButtonAnimator;
var persistentFooterButtons;
var drawer;
var endDrawer;
var bottomNavigationBar;
var bottomSheet;
var backgroundColor;
var resizeToAvoidBottomPadding;
var resizeToAvoidBottomInset;
var primary;
var drawerDragStartBehavior;
var extendBody;
var extendBodyBehindAppBar;
var drawerScrimColor;
var drawerEdgeDragWidth;
MScaffold({
this.key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null),
assert(
!((key!=null
&&key.value is Map<String,dynamic>)
&&key.value.length==1
&&key.value.containsKey('MScaffoldAutoKey')),
"The Key you use for MScaffold cannot be a Map object that contains only one index "
"named, 'MScaffoldAutoKey,' as this is reserved for MScaffold."
),
super(key: key) {
if (key == null) this.key = _autoKeyGen();
}
static List<Key> _autoKeys = [];
bool _usesAutoKey = false;
Key _autoKeyGen() {
_usesAutoKey = true;
Key retKey = ValueKey({'MScaffoldAutoKey': _autoKeys.length});
_autoKeys.add(retKey);
return retKey;
}
@override
MScaffoldState createState() {
return MScaffoldState(
key: key,
appBar: appBar,
body: body,
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButtonAnimator: floatingActionButtonAnimator,
persistentFooterButtons: persistentFooterButtons,
drawer: drawer,
endDrawer: endDrawer,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
backgroundColor: backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
primary: primary,
drawerDragStartBehavior: drawerDragStartBehavior,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
drawerScrimColor: drawerScrimColor,
drawerEdgeDragWidth: drawerEdgeDragWidth,
autoKeys: _usesAutoKey ? _autoKeys : null,
);
}
}
class MScaffoldState extends ScaffoldState {
Key key;
var appBar;
var body;
var floatingActionButton;
var floatingActionButtonLocation;
var floatingActionButtonAnimator;
var persistentFooterButtons;
var drawer;
var endDrawer;
var bottomNavigationBar;
var bottomSheet;
var backgroundColor;
var resizeToAvoidBottomPadding;
var resizeToAvoidBottomInset;
var primary;
var drawerDragStartBehavior;
var extendBody;
var extendBodyBehindAppBar;
var drawerScrimColor;
var drawerEdgeDragWidth;
var autoKeys;
MScaffoldState({
this.key,
this.appBar,
this.body,
this.floatingActionButton,
this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.persistentFooterButtons,
this.drawer,
this.endDrawer,
this.bottomNavigationBar,
this.bottomSheet,
this.backgroundColor,
this.resizeToAvoidBottomPadding,
this.resizeToAvoidBottomInset,
this.primary = true,
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,
this.extendBodyBehindAppBar = false,
this.drawerScrimColor,
this.drawerEdgeDragWidth,
this.autoKeys,
}) : assert(primary != null),
assert(extendBody != null),
assert(extendBodyBehindAppBar != null),
assert(drawerDragStartBehavior != null);
Function() _listenerShow;
Function() _listenerHide;
@override
void initState() {
super.initState();
_listenerShow = () {
if (mounted) {
Scaffold.of(_scaffoldContext)
.showSnackBar(ShowSnackBar().currentSnackBar)
.closed
.then((SnackBarClosedReason reason) {
ShowSnackBar().isSnackBarVisible = false;
ShowSnackBar().lastHideTime = DateTime.now().millisecondsSinceEpoch;
});
}
};
_listenerHide = () {
if (mounted) {
Scaffold.of(_scaffoldContext).hideCurrentSnackBar();
}
};
Future.microtask(() {
if (ShowSnackBar().isSnackBarVisible) _listenerShow();
ShowSnackBar().showNotifier.addListener(_listenerShow);
ShowSnackBar().hideNotifier.addListener(_listenerHide);
});
}
@override
dispose() {
ShowSnackBar().showNotifier?.removeListener(_listenerShow);
ShowSnackBar().hideNotifier?.removeListener(_listenerHide);
autoKeys?.remove(key);
super.dispose();
}
BuildContext _scaffoldContext;
@override
Widget build(BuildContext context) {
return Scaffold(
key: key,
appBar: appBar,
body: Builder(
builder: (context) {
_scaffoldContext = context;
return body;
},
),
floatingActionButton: floatingActionButton,
floatingActionButtonLocation: floatingActionButtonLocation,
floatingActionButtonAnimator: floatingActionButtonAnimator,
persistentFooterButtons: persistentFooterButtons,
drawer: drawer,
endDrawer: endDrawer,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
backgroundColor: backgroundColor,
resizeToAvoidBottomPadding: resizeToAvoidBottomPadding,
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
primary: primary,
drawerDragStartBehavior: drawerDragStartBehavior,
extendBody: extendBody,
extendBodyBehindAppBar: extendBodyBehindAppBar,
drawerScrimColor: drawerScrimColor,
drawerEdgeDragWidth: drawerEdgeDragWidth,
);
}
}
Upvotes: 0
Reputation: 8714
I haven't tried this code myself, but how about you create a ChangeNotifier
to be used with Provider
and attach it above your MaterialApp
:
class MyErrorChangeNotifier extends ChangeNotifier {
String error;
setError(String error) {
this.error = error;
notifyListeners();
}
}
And then create a custom Scaffold
that will display a SnackBar
whenever you call setError
if the Scaffold
is mounted:
class MyScaffold extends Scaffold {
// TODO constructor
@override
ScaffoldState createState() => MyScaffoldState();
}
class MyScaffoldState extends ScaffoldState {
MyErrorChangeNotifier _myErrorCN;
Function() _listener;
@override
void initState() {
super.initState();
_listener = () {
if (mounted) {
showSnackBar(SnackBar(content: Text(_myErrorCN.error)));
}
};
Future.microtask(() {
_myErrorCN = Provider.of<MyErrorChangeNotifier>(context, listen: false)..addListener(_listener);
});
}
@override
void dispose() {
_myErrorCN?.removeListener(_listener);
super.dispose();
}
}
Whenever you have this use case of , try to think of a solution using Provider - the data is sent up the context hierarchy to a ChangeNotifier, which then sends the data back down the context hierarchy to all widgets listening to it.
Upvotes: 1