redux is a bit clunky when it comes to "one-time errors". In general there are 2 ways to handle it:
- 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.
- 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,
);
},
);
}
}