0

I am trying to track current Scaffolds (their BuildContexts) 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 Scaffolds. I did not succeed, however, as my current attempt has two issues:

  1. It does not properly store the current Scaffolds
  2. Apparently the dispose method is too late for removing the Scaffold's BuildContext from the List of current Scaffolds' BuildContexts, so this presents me with the Exception, "Looking up a deactivated widget's ancestor is unsafe."

Here is my current attempt:

  1. The implementation (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();
            },
          ),
        ],
      ),
    );
  }
}
  1. The library classes: 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 Scaffolds' contexts so I can easily display SnackBar messages, regardless of what page the user is on.

JVE999
  • 3,327
  • 10
  • 54
  • 89
  • This seems like it is going to greatly overcomplicate the design of your app. What's wrong with just using `Scaffold.of`? – Abion47 Feb 07 '20 at 23:41
  • "in order to create an app-wide SnackBar function" Why? What's going to trigger the need to show a snack bar? – Ted Henry Feb 08 '20 at 00:26
  • For server-related messages that have to do with the connection, the `SnackBar` should display regardless of what page is currently showing. Using it the normal way requires creating a different method for every `Scaffold` – JVE999 Feb 08 '20 at 01:09

2 Answers2

1

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.

Ovidiu
  • 8,204
  • 34
  • 45
  • `Provider` is still a bit confusing to me. I tried your code, and assigned the `MaterialApp` to be a `child` of `Provider`, i.e., `Provider(create: (_) => MyErrorChangeNotifier(), child: MaterialApp(...` and I now receive the error, "`Unhandled Exception: Tried to use Provider with a subtype of Listenable/Stream (MyErrorChangeNotifier).`," suggesting I use `ListenableProvider`, `ChangeNotifierProvider`, `ValueListenableProvider`, or `StreamProvider` – JVE999 Feb 08 '20 at 17:35
  • I solved that last issue by changing `Provider(create: (_) => MyErrorChangeNotifier(), child: MaterialApp(...` to `ChangeNotifierProvider<...`, however, when I try to show a `SnackBar` with `Provider.of(context, listen: false).setError("test3");`, nothing happens and nothing is listed in the log. – JVE999 Feb 08 '20 at 19:33
  • 1
    Using `ChangeNotifierProvider` is correct in your case. When you say nothing happens, do you mean the listener attached by the custom scaffold is not triggered? Try creating an instance of `MyErrorChangeNotifier` in the `initState` of the top level widget that builds the `MaterialApp`, and use the `ChangeNotifierProvider.value` constructor with it, just to make sure that you are not reconstructing different providers. If you put a breakpoint on `notifyListeners()`, you should be able to inspect `this` and see how many listeners are attached - there should be >=1 if you replaced all scaffolds – Ovidiu Feb 09 '20 at 09:00
  • I found it was working, just `showScaffold()` wasn't doing anything. Using the `context` of the `Scaffold`, gotten by wrapping `Scaffold` in a `Builder(...` and setting a variable to its `context`, I could get the `SnackBar` to display. However, I found I don't have a way to hide the `SnackBar`. `ChangeNotifier` only provides one possible notification, without any variables. So, I created two `ChangeNotifier` properties: `showNotifier` and `hideNotifier` and used those to notify listeners of the events. Thanks for the help! I got this figured out in a pretty cool way. I'll post the answer – JVE999 Feb 10 '20 at 23:32
0

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 Scaffolds to MScaffolds.

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,
    );
  }
}
JVE999
  • 3,327
  • 10
  • 54
  • 89