Bread Breeder
Bread Breeder

Reputation: 144

How to access the most recent scaffold in Flutter from the root ancestor?

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

Answers (2)

user7653815
user7653815

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

Bread Breeder
Bread Breeder

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

Related Questions