Trouble
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:
- runZonedGuarded
- ... async{ await future() }catch(e){ ... }
- Future.onError
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.
My (bearable) solution
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; //!
...
}
}
flaws
- At this moment I don't know how organize app with several pages.
materialContextcan be taken only fromMaterialAppchilds, but pages are set already at theMaterialAppwidget. Maybe, I will injectZonedCatcherin all pages and building pages will re-setmaterialContext. But I probably will face withGlobalKey's problems, like resetingmaterialContextby some pages at the same time on gestures. - It is not common pattern, I have to thoroughly document this moment and this solution makes project harder to understand by others programmists.
- This solution is not foreseen by Flutter creators and it can break on new packages with breaking-changes.