I build Flutter app + Dart.
Now i am trying to catch all future exceptions in ONE place (class) AND showAlertDialog
.
Flutter Docs proposes 3 solutions to catch async
errors:
But no one can achieve all of the goals (in its purest form):
First: can't run in widget's build
(need to return Widget
, but returns Widget?
.
Second: works in build
, but don't catch async errors, which were throwed by unawaited futures, and is"dirty" (forces to use WidgetBinding.instance.addPostFrameCallback
. I can ensure awaiting futures (which adds to the hassle), but I can't check does ensures it third-part libraries. Thus, it is bad case.
Third: is similar to second. And looks monstrous.
I get first solution and added some details. So,
I created ZonedCatcher
, which shows AlertDialog
with exception or accumulates exceptions if it doesn't know where to show AlertDialog
(BuildContext
has not been provided).
AlertDialog
requires MaterialLocalizations
, so BuildContext
is taken from MaterialApp
's child MaterialChild
.
void main() {
ZonedCatcher().runZonedApp();
}
...
class ZonedCatcher {
BuildContext? _materialContext;
set materialContext(BuildContext context) {
_materialContext = context;
if (_exceptionsStack.isNotEmpty) _showStacked();
}
final List<Object> _exceptionsStack = [];
void runZonedApp() {
runZonedGuarded<void>(
() => runApp(
Application(
MaterialChild(this),
),
),
_onError,
);
}
void _onError(Object exception, _) {
if (_materialContext == null) {
_exceptionsStack.add(exception);
} else {
_showException(exception);
}
}
void _showException(Object exception) {
print(exception);
showDialog(
context: _materialContext!,
builder: (newContext) => ExceptionAlertDialog(newContext),
);
}
void _showStacked() {
for (var exception in _exceptionsStack) {
_showException(exception);
}
}
}
...
class MaterialChild extends StatelessWidget {
MaterialChild(this.zonedCatcher);
final ZonedCatcher zonedCatcher;
@override
Widget build(BuildContext context) {
zonedCatcher.materialContext = context; //!
...
}
}
materialContext
can be taken only from MaterialApp
childs, but pages are set already at the MaterialApp
widget. Maybe, I will inject ZonedCatcher
in all pages and building pages will re-set materialContext
. But I probably will face with GlobalKey
's problems, like reseting materialContext
by some pages at the same time on gestures.By default, if there is an uncaught exception in a Flutter application, it is passed to FlutterError.onError
. This can be overridden with a void Function(FlutterErrorDetails)
to provide custom error handling behaviour:
void main() {
WidgetsFlutterBinding.ensureInitialized();
FlutterError.onError = (details) {
print(details.exception); // the uncaught exception
print(details.stack); // the stack trace at the time
}
runApp(MyApp());
}
If you want to show a dialog in this code, you will need access to a BuildContext
(or some equivalent mechanism to hook into the element tree).
The standard way of doing this is with a GlobalKey
. Here, for convenience (because you want to show a dialog) you can use a GlobalKey<NavigatorState>
:
void main() {
WidgetsFlutterBinding.ensureInitialized();
final navigator = GlobalKey<NavigatorState>();
FlutterError.onError = (details) {
navigator.currentState!.push(MaterialPageRoute(builder: (context) {
// standard build method, return your dialog widget
return SimpleDialog(children: [Text(details.exception.toString())]);
}));
}
runApp(MyApp());
}
Note that if you need a BuildContext
inside your onError
callback, you can also use navigator.currentContext!
.
You then need to pass your GlobalKey<NavigatorState>
to MaterialApp
(or Navigator
if you create it manually):
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: navigatorKey, // pass in your navigator key
// other fields
);
}