Writing in the front

In the section on Flutter exception capture in the book “Flutter In Action”, which describes how to catch asynchronous exceptions, the use of zones is mentioned.

To facilitate understanding, readers can compare a Zone to a code execution sandbox. Different sandboxes are isolated from each other. Sandboxes can capture, intercept or modify some code behaviors, such as log output, Timer creation and micro-task scheduling. The Zone can also catch all unhandled exceptions.

This article is mainly to understand the usage and parameters of Zone.

content

Let’s start with the exception example

In Flutter, exceptions in synchronized code are caught using a try-catch. We await Future, which is also synchronous code, so it can also be caught by try-catch:

main() async {
  try {
    throw "My Error";
  } catch (e) {
    print(e);
  }

  try {
    await Future.error("My Future Error");
  } catch (e) {
    print(e); }}Copy the code

Print result:

My Error
My Future Error
Copy the code

For asynchronous errors that are not written synchronously, we need to use zones to handle them. A Zone provides an environment for code to capture information within. As an example given in the book Flutter In Action, it forks the current Zone into a new Zone using the runZoned() method in Dart and executes the code there.

R runZoned<R>(R body(),
    {Map<Object?.Object?>? zoneValues,
    ZoneSpecification? zoneSpecification,
    @Deprecated("Use runZonedGuarded instead") Function? onError}) {
  checkNotNullable(body, "body"); .return _runZoned<R>(body, zoneValues, zoneSpecification);
}

R _runZoned<R>(R body(), Map<Object?.Object?>? zoneValues,
        ZoneSpecification? specification) =>
    Zone.current
        .fork(specification: specification, zoneValues: zoneValues)
        .run<R>(body);
Copy the code

RunZoned () is a zone.current. Fork ().run() package.

So by covering our entire App with a Zone, we can catch all exceptions.

runZoned(() {
    runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
    var details=makeDetails(obj,stack);
    reportError(details);
});

Copy the code

Zone

main

When we run main(), we are already inside a root Zone:

main() {
  Zone.root.run(() => print("hello"));
  print(Zone.root == Zone.current);
}
Copy the code

Print result:

hello
true
Copy the code

Calling zone.root.run () from main() is the same as calling it from main(), and since it’s already running by default, we can’t make any custom changes to the Zone, which is why we need to create a new Zone to handle it.

parameter

There are several parameters in the Zone:

  • zoneValues
  • zoneSpecification
  • onError

zoneValues

This is the Zone’s private data, which can be obtained through the Zone [key] instance, and can be inherited by its own child zones.

  Zone parentZone = Zone.current.fork(zoneValues: {"parentValue": 1});
  Zone childZone = parentZone.fork();
  // childZone can obtain the corresponding value from the parent's key
  childZone.run(() => {print(childZone["parentValue"])});
Copy the code

zoneSpecification

Zone configurations allow you to customize code behaviors, such as blocking log output behaviors.

abstract class ZoneSpecification { ... const factory ZoneSpecification( {HandleUncaughtErrorHandler? handleUncaughtError, RunHandler? run, RunUnaryHandler? runUnary, RunBinaryHandler? runBinary, RegisterCallbackHandler? registerCallback, RegisterUnaryCallbackHandler? registerUnaryCallback, RegisterBinaryCallbackHandler? registerBinaryCallback, ErrorCallbackHandler? errorCallback, ScheduleMicrotaskHandler? scheduleMicrotask, CreateTimerHandler? createTimer, CreatePeriodicTimerHandler? createPeriodicTimer, PrintHandler? print, ForkHandler? fork}) = _ZoneSpecification; . }Copy the code
Modify the behavior of print

For example, in a Zone, if we want to change its print behavior, we can do this:

main() {
  Zone parentZone = Zone.current.fork(specification: new ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    parent.print(zone, "Intercepted: $line");
     // Print can also be written to a file here and sent to the server
  }));
  parentZone.run(() => print("hello"));
  print("hello");
}
Copy the code

Print result:

Intercepted: hello
hello
Copy the code

It can be seen that when parentZone is running, the behavior of print has been modified.

Since the last print is not in the same Zone, this print will not change.

Modify the behavior of run

If we want to do something when we enter run(), we can:

main() {
  Zone parentZone = Zone.current.fork(
      specification: new ZoneSpecification(
      run: <R>(self, parent, zone, f) {
    print("Enter run");
    return parent.run(zone, f);
  }));
  parentZone.run(() => print("hello"));
}
Copy the code

Print result:

Enter run
hello
Copy the code
Modify the registration callback
main() {
  Zone.root;
  Zone parentZone = Zone.current.fork(specification:
      new ZoneSpecification(registerCallback: <R>(self, parent, zone, f) {
    // Call the callback we actually registered. The first time f comes in is start() and the second time is end().
    f();
    return f;
  }));
  parentZone.run(() {
    Zone.current.registerCallback(() => start());
    print("hello");
    Zone.current.registerCallback(() => end());
  });
}

void start() {
  print("start");
}

void end() {
  print("end");
}
Copy the code

Print result:

start
hello
end
Copy the code

onError

Although the “runZoned()” methods had onError callbacks, the official system recommended “runZonedGuarded()” instead.

 runZonedGuarded(() {
    throw "null"; },Object error, StackTrace stack) {
    print("error = ${error.toString()}");
  });
Copy the code

Print the result

error = null
Copy the code

This means that any exceptions in the Zone that we don’t catch will go to the onError callback. If the Zone specification implements handleUncaughtError or the onError callback, then the Zone becomes an error-zone.

Exceptions that occur in an error-zone do not cross boundaries, for example:

var future = new Future.value(499); var future2 = future.then((_) {throw "future error"; }); runZonedGuarded(() { var future3 = future2.catchError((e) { print(e.toString()); // will not print}); }, (Object error, StackTrace stack) { print(" error = ${error.toString()}"); // will not print});Copy the code

The future function is defined outside of the error-zone and will throw an exception after execution. When called from the error-zone, the exception will not be caught by the error-zone because it has exceeded its bounds. The solution is to set a Zone in the future, so that the error is caught by the error-zone outside:

  var future = new Future.value(499);
  runZonedGuarded(() {
    var future2 = future.then((_) {
      throw "future error";
    });
    runZonedGuarded(() {
      var future3 = future2.catchError((e) {
        print(e.toString());
      });
    }, (Object error, StackTrace stack) {
      print(" error = ${error.toString()}");
    });
  }, (Object error, StackTrace stack) {
    print("out error = ${error.toString()}");
  });
Copy the code

Output result:

out error = future error
Copy the code