2

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.

Bread Breeder
  • 144
  • 1
  • 6
  • I think it would be useful if you added some example code – Azsgy Apr 22 '18 at 15:54
  • @Azsgy as you requested I've added some example code. – Bread Breeder Apr 22 '18 at 16:25
  • @RémiRousselet I added an explanation describing why the post isn't a duplicate. Please remove the flag about duplication. Also please tell me, if the post needs further clarification. – Bread Breeder Apr 22 '18 at 22:04
  • I still don't understand how that is different to the example provided by https://stackoverflow.com/questions/45948168/how-to-create-toast-in-flutter/45948243#45948243. The answer is still the same : `Scaffold.of(context).showSnackbar(snackbar)`. The only difference is that you need to pass `context` as parameter of `addDocumentListener`. – Rémi Rousselet Apr 23 '18 at 00:11
  • @RémiRousselet I hope the newest edit clarifies why passing the current Scaffold's context to `addDocumentListener` is insufficient – Bread Breeder Apr 23 '18 at 08:15
  • Still the same thing. Just use the context of a widget inside the new page – Rémi Rousselet Apr 23 '18 at 08:59
  • @RémiRousselet that would be correct if I were to call `addDocumentListener` again but I don't. It's only ever called on the HomePage. At that point the callback can access the HomePage's Scaffold and display the SnackBar but only if the user didn't go to another page. If the user goes to another page the old callback is still installed but the SnackBar won't be displayed due to only having access to the HomePage's Scaffold. – Bread Breeder Apr 23 '18 at 09:08
  • I understand now. But I have a few more questions/change before removing the flag. [Let's talk here](https://chat.stackoverflow.com/rooms/169573/discussion-between-remi-rousselet-and-bread-breeder). – Rémi Rousselet Apr 23 '18 at 09:42

2 Answers2

1

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);
    });
  }
}
Bread Breeder
  • 144
  • 1
  • 6
1

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()
        ),
      }
  });
}
user7653815
  • 186
  • 1
  • 6