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

前端 未结 1 1229
萌比男神i
萌比男神i 2021-01-05 08:02

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:

1条回答
  •  时光说笑
    2021-01-05 08:40

    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 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 {
      StreamSubscription _errorsSubscription;
      final _scaffoldKey = GlobalKey();
    
      @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 syncErrors; // coming from your store/middleware
    
      MyApp({Key key, this.syncErrors}) : super(key: key);
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State {
      StreamSubscription _errorsSubscription;
      final _errorScaffoldKey = GlobalKey();
    
      @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,
            );
          },
        );
      }
    }
    
        

    0 讨论(0)
    提交回复
    热议问题