JVE999
JVE999

Reputation: 3517

Flutter: Modifying Scaffold to track currently displayed Scaffolds' BuildContexts for app-wide SnackBar methods

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.

Upvotes: 0

Views: 2624

Answers (2)

JVE999
JVE999

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 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,
    );
  }
}

Upvotes: 0

Ovidiu
Ovidiu

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

Related Questions