1. Why write this article
  2. Classification of Flutter anomalies
  3. Capture and treatment of Flutter abnormalities
  4. How is exception catching for a particular business handled?
  5. Why doesn’t a flutter crash when it triggers a DART anomaly
  6. Why are some exceptions not caught when sandboxed internally in a program?

Why write this article

The whole process of flutter exception handling is relatively simple, mainly including zone(sandbox), try/catch and Future asynchronous exception catching. The reason for writing this article is that when developing flutter dynamic, special processing is required for dynamic widgets. For example, a single widget degrades, a page degrades without affecting other businesses, and so on. So comb through your knowledge about Flutter exception and make a note.

Classification of flutter anomalies

  • Native exception
    • Plugin exceptions, such as those triggered by Java on the Android side and ObjectC on the Ios side, will cause the program to blink back. The processing method is the same as other exceptions of Native, and these exceptions are not within the scope of this article
  • The Dart abnormal
    • A Zone is an environment for code execution. Different sandboxes are isolated from each other. A Zone can catch both synchronous and asynchronous exceptions, mainly those that are not caught by the Flutter framework and application code
      • Synchronous exception: An exception that occurs during normal code execution and can be caught by a try/catch or by a Zone
      • Asynchronous exceptions: try/catch cannot be caught, but can be caught by a Future catchError, or by a Zone
  1. A flutter framework exception is an exception triggered by the framework while it is running. This exception has already been caught by the framework

  2. Widget build exception is also an exception of the Flutter framework. Because it happens in the build phase, the framework returns ErrorWidget to us during the process of the build. This kind of exception is what we encounter most during development


Take a look at a few examples, respectively Zone, synchronous exception, asynchronous exception

// Sandboxes runZonedGuarded<Future<Null>>(() async {runApp(MyApp())); }, (error, stackTrace) async {// Print ("expectinTest zone error ${stacktrace.toString ()}"); }); String c; Future.value(1).then((value) {null print("app async error ${c.length}"); }). CatchError ((error){print(error); }); Try {print("expectinTest app error ${c.string}"); } catch (e) { print(e); } // Try {future.value (1). Then ((value) {null print("app async error ${c.length}"); }); } catch (e) { print(e); }Copy the code

Capture and treatment of flutter abnormalities

The three main points are as follows

  1. How does the Flutter Framework catch exceptions
  2. How are Widget Build exceptions handled with ErrorWidget
  3. How does a Zone catch exceptions

How does the Flutter Framework catch exceptions and widget Build exceptions handled with ErrorWidget

Speaking of this, we have to say that the three trees of flutter, widget-element-render, are the widgets we come into contact with most in daily development. Among the widgets, the StatefulWidget and StateLessWidget are the ones we are most familiar with. Their corresponding elements are StatefulElement and StatelessElement, and their common ancestor is ComponentElement, Note the performRebuild method for ComponentElement.

// For StatefulWidget and StateLessWidget, the build method they execute is @override void performRebuild() {Widget built; Built = build(); StateLessWidget build(); DebugWidgetBuilderValue (Widget, built); } catch (e, stack) { _debugDoingBuild = false; ErrorWidget Built = errorWidget. builder(// FlutterError. ReportError (details) is called in _debugReportException; _debugReportException( ErrorDescription('building $this'), e, stack, informationCollector: () sync* { yield DiagnosticsDebugCreator(DebugCreator(this)); },),); } finally { _dirty = false; assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false)); }} // FlutterError. ReportError FlutterErrorDetails _debugReportException( DiagnosticsNode context, dynamic exception, StackTrace stack, { InformationCollector informationCollector, }) { final FlutterErrorDetails details = FlutterErrorDetails( exception: exception, stack: stack, library: 'widgets library', context: context, informationCollector: informationCollector, ); FlutterError.reportError(details); return details; }Copy the code

In the above source code, we found several key points, namely build(), try/catch wrapped around build(), ErrorWidget, and FlutterError. ReportError

  1. Build (), the build method that inherits StatefulWidget and StateLessWidget, is called from here. Obviously, the framework does try/catch protection when calling build methods. This is the exception of the Flutter framework, which of course protects much more than this, but already contains most of the errors
  2. ErrorWidget. You can see that when the build method fails, the system will return the ErrorWidget by default. This ErrorWidget is the red screen we see in daily development
  3. FlutterError. ReportError, flutter framework exceptions are reported via this method. The default implementation within the system is to print errors to the console

Exception handling of the Flutter framework

FlutterError.reportError

If you look at the source code, you’ll see that the default implementation is a static function, which eventually leads to the onError method

/// _debugReportException calls reportError static void reportError(FlutterErrorDetails details) {assert(details! = null); assert(details.exception ! = null); if (onError ! = null) onError(details); // The default implementation of onError is dumpErrorToConsole, DumpErrorToConsole static FlutterExceptionHandler onError = dumpErrorToConsole;Copy the code

FlutterExceptionHandler (); FlutterExceptionHandler (); FlutterExceptionHandler (); FlutterExceptionHandler (); Something like this

final defaultOnError = FlutterError.onError; Fluttererror. onError = (FlutterErrorDetails details) {FlutterErrorDetails = (FlutterErrorDetails details) {flutterError. onError = (FlutterErrorDetails details) { };Copy the code
ErrorWidget

This is actually LeafRenderObjectWidget. By the way, there are many kinds of widgets, such as common layout widgets, such as StatelessWidgets, statefull Widgets, RenderObjectWidget (RenderObjectWidget) is a subclass of RenderObjectWidget (RenderObjectWidget), similar to ErrorWidget and RawImage (RawImage), which are the actual rendering classes for images. ErrorWidget renders the error stack to the screen, so the default implementation of the system is to make a try/catch protection on the build key and return the ErrorWidget in the catch protection.

// If you want to do that, Errorwidget. builder Built = errorWidget. builder(_debugReportException(ErrorDescription('building $this'), e, stack, informationCollector: () sync* { yield DiagnosticsDebugCreator(DebugCreator(this)); },),); / / ErrorWidgetBuilder builder is a static function static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder; / / _defaultErrorWidgetBuilder source posted, I don't realize is returned to the ErrorWidgetCopy the code

ErrorWidgetBuilder Builder is a static function. ErrorWidgetBuilder is a static function. ErrorWidgetBuilder builder is a static function

var defaultErrorBuilder = ErrorWidget.builder; ErrorWidget.builder = (FlutterErrorDetails details) { Widget errorBuilder; ErrorBuilder = TerrorBuilder (details); errorBuilder = TerrorBuilder (details); errorBuilder = terrorBuilder (details); ErrorBuilder = buildErrorWidget(details);} else{errorBuilder = buildErrorWidget(details); } return errorBuilder; };Copy the code
How does a Zone catch exceptions

Zone can catch both synchronous and asynchronous exceptions that are not handled in code. We usually write runApp wrapped inside it, but have you ever wondered why flutter is recommended to be written that way?

runZonedGuarded<Future<Null>>(() {
    runApp(MyApp());
  }, (error, stackTrace) {
    print("expectinTest zone error ${stackTrace.toString()}");
  });
Copy the code

Then look at runZonedGuarded

@Since("2.8") R runZonedGuarded<R>(R body(), void onError(Object error, StackTrace stack), {Map zoneValues, ZoneSpecification ZoneSpecification}) {// omit some code try {// Return _runZoned<R>(body, zoneValues, zoneSpecification) with a try/catch package. } catch (error, stackTrace) {// If an exception is raised, execute the onError callback, in this case the synchronization exception onError(error, stackTrace); } return null; }Copy the code

The source system kept a try/catch system for the system when runZone was actually executed. When synchronization exceptions were triggered, the onError callback was used. This was why runZonedGuarded could catch synchronization exceptions. So how are asynchronous exceptions caught? Still have to look at Future source code.

Value (1). Then ((value) {c is null print("app Async error ${c.length}"); }); Static void _propagateToListeners(_Future source, _FutureListener listeners) { while (true) { assert(source._isComplete); bool hasError = source._hasError; AsyncError AsyncError = source._error; // If there is an error, call handleUncaughtError on the zone of _Future, The onError method source._zone. handleUncaughtError(Asynceror.error, asynceror.stackTrace) is eventually called; } return; }} // how to create a zone for _Future? _Future() : _zone = zone.current; ZoneValue (T value, this._zone) {_setValue(value); // Zone _future.zonevalue (T value, this._zone) {_setValue(value); }Copy the code

Question to consider: Zone protects a piece of other code. If there is asynchronous code in this code, and this asynchronous code throws an error, does Zone exception catching still work? This question will be answered in question 6. At this point, we have answered questions 1-3.

How are business-specific exceptions handled?

Recently, there was a business scenario about the need to hook the build exception of a particular Widget so that it would not affect the degradation and error reporting of the main business.

ErrorWidgetBuilder is a custom ErrorWidgetBuilder, which can be used to determine the wrong stack, context and other information in the build callback. If a hook is needed, the custom ErrorWidget is returned. If not, it is thrown to the upper level

Future<void> _setErrorWidgetBuilder() async { var defaultErrorBuilder = ErrorWidget.builder; ErrorWidget.builder = (FlutterErrorDetails details) { if (buildErrorWidget == null) { return defaultErrorBuilder(details); } // If there is no dynamic exception, the default if (! checkIfSpecialException(details)) { print("expectinTest normal build error"); return defaultErrorBuilder(details); } Widget errorBuilder; errorBuilder = buildErrorWidget(); // Build your own error exception return errorBuilder; }; } Set<String> _hookClassNameSet = Set<String>.. add("a").. add("b"); bool checkIfHookException(FlutterErrorDetails details) { if (isEmpty(_hookClassNameSet)) return false; if (details == null) { return false; } // Check whether the context contains the special class name if (details.context! = null) { bool isHookError = false; _hookClassNameSet.forEach((element) { if (details? .context? .toString()? .contains(element) ?? false) { isHookError = true; }}); if (isHookError) return true; } // Process the stack into a List and loop to see if it contains the special class name Iterable<String> lines = details? .stack? .toString()? .trimRight()? .split('\n') ?? []; if (isEmpty(lines)) { return false; } final List<StackFrame> parsedFrames = StackFrame.fromStackString(lines.join('\n')); for (int index = 0; index < parsedFrames.length; index += 1) { final StackFrame frame = parsedFrames[index]; final String className = '${frame.className}'; if (_hookClassNameSet.contains(className)) { return true; } } return false; }Copy the code

Why doesn’t a flutter collapse when it triggers an anomaly?

This is related to the message loop mechanism of FLUTTER. There are two tasks, one is microtask and the other is event. Each task has its own queue and is independent of each other

Why are some exceptions not caught when sandboxed internally in a program?

This is an interesting problem. Suppose you have a scenario where a single Zone is created for a widget’s build function, but when you run the Future in the build and throw it wrong, the Zone is unable to catch the exception. The reason is to see the runZone source code

String c; RunZonedGuarded <Future<Null>>(() Async {future.value (1). Then ((value) {// Async error ${c. ring}"); }); }, (error, stackTrace) async { print("expectinTest zone error ${stackTrace.toString()}"); }); try { return _runZoned<R>(body, zoneValues, zoneSpecification); } catch (error, stackTrace) { onError(error, stackTrace); } /// Runs [body] in a new zone based on [zoneValues] and [specification]. R _runZoned<R>(R body(), Map zoneValues, ZoneSpecification specification) => Zone.current .fork(specification: specification, zoneValues: zoneValues) .run<R>(body)Copy the code

As you can see, the protection period only lasts as long as the body parameter is executed in _runZoned. When we protect a build for a particular widget, the build is executed immediately, which means the protection is over. The same applies to asynchronous exceptions. The protection period of the current custom zone has expired, so it cannot be captured. That’s why the runZone examples we’ve seen run in the main function. Because runApp() actually starts the main thread of the Flutter, the thread will continue to run unless the user exits the APP