Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to show a SnackBar from async executions when no context is available?

Tags:

flutter

I have an application that uses Flutter-Redux. On certain user actions various redux-actions are dispatched and those run asynchronously in a middleware. My question now is: how can I show a SnackBar from the middleware in case something goes wrong. I cannot use callbacks because it is not guaranteed that the Widget that dispatched the actions is still available. The error would be:

At this point the state of the widget's element tree is no longer stable. To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling inheritFromWidgetOfExactType() in the widget's didChangeDependencies() method.

Example:

  1. User clicks "Save".
  2. Action "Save to DB" is dispatched async.
  3. Action "Save over Rest" is dispatched async.
  4. Dialog is closed.

Now... When e.g. the rest call returns with an error, the dialog is closed since a "long" time, its context is invalid and I have no possibility to show a SnackBar.

Further:

A SnackBar must always be bound to a Scaffold. So I already tried to make an empty root-Scaffold and reference it by a GlobalKey. This brings the problem that the SnackBar is hidden when another widget lies over the root widget and cannot be read by the user.

Any suggestions how to solve this issue?

Best regards, Florian

like image 963
Florian Huonder Avatar asked Sep 07 '18 13:09

Florian Huonder


1 Answers

redux is a bit clunky when it comes to "one-time errors". In general there are 2 ways to handle it:

  1. You can save the errors in the store and display an error overlay while there is an error in the store. Remove the error from the store to dismiss the overlay.
  2. Consider the error display as a "one-time" side-effect (just like playing a sound). I think this is the better solution, especially if you want to use snackbars.

I'm not sure how exactly your middleware looks like, but after the network request failed, you would push the error object into a rxdart Subject or StreamController. Now you have a Stream of errors.

As a direct child of your StoreProvider, create your own InheritedWidget that holds the stream of errors, named SyncErrorProvider:

class SyncErrorProvider extends InheritedWidget {
  const SyncErrorProvider({Key key, this.errors, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  final Stream<Object> errors;

  static SyncErrorProvider of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(SyncErrorProvider) as SyncErrorProvider;
  }

  @override
  bool updateShouldNotify(SyncErrorProvider old) => errors != old.errors;
}

The inherited widget should wrap your MaterialApp. Now you have a simple way to access the stream of errors from any route, using SyncErrorProvider.of(context).errors from didChangeDependencies.


Displaying the error in a snackbar is a bit of a challenge, because the position of a snackbar depends on the page layout (FAB, bottom navigation...), and sometimes an appearing snackbar moves other UI elements.

The best way to handle the snackbar creation really depends on your app. I'm also not sure how often these errors would occur, so maybe don't spend too much time on it.

Two different approaches with advantages and disadvantages:

Display errors in page scaffolds

In every screen that has a scaffold, listen to the stream of errors and display snackbars in the local scaffold. Make sure to unsubscribe when the widgets are disposed.

Advantage of this approach is that the snackbars are a part of the page UI and will move other elements of the scaffold.

Disadvantage is that if there are dialogs or screens without a scaffold, the error will not be visible.

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  StreamSubscription _errorsSubscription;
  final _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if(_errorsSubscription == null) {
      _errorsSubscription = SyncErrorProvider.of(context).errors.listen((error) {
        _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(error.toString())));
      });
    }
  }

  @override
  void dispose() {
    _errorsSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      body: ...,
    );
  }
}

Have a global scaffold for error snackbars

This scaffold would only be used for snackbars, nothing else. Advantage is that the errors are always guaranteed to be visible, disadvantage that they will overlap FABs and bottom bars.

class MyApp extends StatefulWidget {
  final Stream<Object> syncErrors; // coming from your store/middleware

  MyApp({Key key, this.syncErrors}) : super(key: key);
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  StreamSubscription _errorsSubscription;
  final _errorScaffoldKey = GlobalKey<ScaffoldState>();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _errorsSubscription = widget.syncErrors.listen((error) {
      _errorScaffoldKey.currentState.showSnackBar(SnackBar(content: Text(error.toString())));
    });
  }

  @override
  void dispose() {
    _errorsSubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, child) {
        Scaffold(
          key: _errorScaffoldKey,
          body: child,
        );
      },
    );
  }
}
like image 95
boformer Avatar answered Oct 13 '22 17:10

boformer