Zones in Dart: Open Heart Surgery for Surroundings

Hello! My name is Dima and I am a frontend developer at Wrike. We write the client part of the project in Dart, but we have to work with asynchronous operations no less than with other technologies. Zones are one of the handy tools Dart provides for this. But in the Dart community you rarely find useful information about it, so I decided to understand and tell you more about this powerful tool.





Disclaimer: All code used in this article is only pretending to be copy-paste. In fact, I simplified it a lot and got rid of details that should not be paid attention to in the context of this article. In preparing the material, we used Dart version 2.7.2 and AngularDart version 5.0.0.

Dart . . , dart:async, .



Wrike ( AngularDart) :



// Part of AngularDart component class
final NgZone _zone;
final ChangeDetectorRef _detector;
final Element _element;

void onSomeLifecycleHook() {
  _zone.runOutsideAngular(() {
    _element.onMouseMove.where(filterEvent).listen((event) {
      doWork(event);
      _zone.run(_detector.markForCheck);
    });
  });
}


Dart- , . , .



, , , :



  • API , .
  • .
  • ( , ).


Dart , issues github. API, , , , , DartUP. , .



, :



  • package:intl;
  • package:quiver;
  • package:angular.


.



Intl



intl โ€” . : , , message plural .



:



class AppIntl {
  static String loginLabel() => Intl.message(
        'Login',
        name: 'AppIntl_loginLabel',
      );
}


. , , . , - . withLocale, :



// User has 'en' as default locale, but he works from Russia
final fallbackLocale = 'ru';

Future<Duration> parseText(String userText) async =>
    // Try to parse user text
    await _parseText(userText) ??
    // Try to parse with 'ru' locale if default parsing failed
    await Intl.withLocale(fallbackLocale, () => _parseText(userText));

// This is actual parser
Future<Duration> _parseText(String userText) {
  // ...
}


-, fallback .



, withLocale , , . !



parseText Future, , . , - , . โ€” , . โ€” . .



, Future , , . .



1. Future



โ€” ! - Future:



class Future {
  Future() : _zone = Zone.current; // Save current zone on creation

  final Zone _zone;
  // ...
}


Future . . , then:



class Future {
  // ...
  Future<R> then<R>(
    FutureOr<R> callback(T value), // ...
  ) {
    // Notify zone about callback for async operation
    callback = Zone.current.registerUnaryCallback(callback);
    final result = Future();
    // Schedule to complete [result] when async operation ends
    _addListener(_FutureListener.then(
      result,
      callback, // ...
    ));
    return result;
  }
}

class _FutureListener {
  // ...
  FutureOr<T> handleValue(S value) =>
      // Call scheduled work inside zone that was saved in [result] Future
      result._zone.runUnary(_callback, value);
}


! , . Future, . Future , โ€” Zone.current. runUnary . , , , . , - !



2. , , ยซ ยป



It's an execution context. โ€” Brian Ford, zone.js author.

โ€” , ยซยป: . , , . Future , , run*. -.



โ€” , _current. Zone.current โ€” _current. :



class Zone {
  static Zone _current = _rootZone; // This is where current zone lives

  static Zone get current => _current;
  // ...
}


, . run* : run, runUnary, runBinary. _current:



class Zone {
  // ...
  R run<R>(R action()) {
    Zone previous = _current;
    // Place [this] zone in [_current] for a while
    _current = this;
    try {
      return action(); // Then do stuff we wanted
    } finally {
      _current = previous; // Then revert current zone to previous
    }
  }
}


_current , . Zone.current .



! , , current , :



class _FutureListener {
  // ...
  FutureOr<T> handleValue(T value) => result._zone.runUnary(_callback, value);
}

class _FutureListener {
  // ...
  FutureOr<T> handleValue(T value) {
    final previousZone = Zone.current;
    Zone.current = result._zone;
    final updatedValue = _callback(value);
    Zone.current = previousZone;
    return updatedValue;
  }
}


. run* , , , . . .



, Intl , . - .



3. Intl



withLocale:



class Intl {
  // ...
  static withLocale(String locale, Function() callback) =>
      // Create new zone with saved locale, then call callback inside it
      runZoned(callback, zoneValues: {#Intl.locale: locale});
}


- ! .



runZoned . , runZoned . run* .



, โ€” zoneValues. , . zoneValues ( Symbol).



, :



class Intl {
  // ...
  static String getCurrentLocale() {
    // Get locale from current zone
    var zoneLocale = Zone.current[#Intl.locale];
    return zoneLocale == null ? _defaultLocale : zoneLocale;
  }
  // ...
}


! , . , . - , .



[], ( โ€” ). []= โ€” , , . - withLocale runZoned:



class Intl {
  // ...
  static withLocale(String locale, Function() callback) =>
      // Create new zone with saved locale, then call callback inside it
      runZoned(callback, zoneValues: {#Intl.locale: locale});
}


, .



, :



// User has 'en' as default locale, but he works from Russia
final fallbackLocale = 'ru';

Future<Duration> parseText(String userText) async =>
    // Try to parse user text
    await _parseText(userText) ??
    // Try to parse with 'ru' locale if default parsing failed
    await Intl.withLocale(fallbackLocale, () => _parseText(userText));

// This is actual parser
Future<Duration> _parseText(String userText) {
  // ...
}


, withLocale , . , Future . _parseText _parseText. !



, Future . Future, Stream Timer ยซยป . , . .



FakeAsync



- -. , . Dart . , test, . , Future test, expect, Future :



void main() {
  test('do some testing', () {
    return getAsyncResult().then((result) {
      expect(result, isTrue);
    });
  });
}


โ€” . , debounce , . ยซยป mock , .



, . , . package:quiver FakeAsync.



:



import 'package:quiver/testing/async.dart'; 

void main() {
  test('do some testing', () {
    // Make FakeAsync object and run async code with it
    FakeAsync().run((fakeAsync) {
      getAsyncResult().then((result) {
        expect(result, isTrue);
      });
      // Ask FakeAsync to flush all timers and microtasks
      fakeAsync.flushTimers();
    });
  });
}


FakeAsync, , . .



, .



1. FakeAsync



run :



class FakeAsync {
  // ...
  dynamic run(callback(FakeAsync self)) {
    // Make new zone if there wasn't any zone created before
    _zone ??= Zone.current.fork(specification: _zoneSpec);
    dynamic result;
    // Call the test callback inside custom zone
    _zone.runGuarded(() {
      result = callback(this);
    });
    return result;
  }
}


โ€” , .



โ€” fork specification.



Dart โ€” root. , โ€” Zone.root. root, root . run, ?



class Zone {
  // ...
  R run<R>(R action()) {
    Zone previous = _current;
    // Place [this] zone in [_current] for a while
    _current = this;
    try {
      return action(); // Then do stuff we wanted
    } finally {
      _current = previous; // Then revert current zone to previous
    }
  }
}


ยซ ยป. :



class _RootZone implements Zone {
  // Only root zone can change current zone
  // ...
  R _run<R>(Zone self, ZoneDelegate parent, Zone zone, R action()) {
    Zone previous = Zone._current;
    // On this [zone] the .run() method was initially called
    Zone._current = zone;
    try {
      return action(); // Then do stuff we wanted
    } finally {
      Zone._current = previous; // Then revert current zone to previous
    }
  }
}


!



โ€” root . , - .



2. zoneSpecification



ZoneSpecification โ€” zoneValues:



abstract class ZoneSpecification {
  // All this handlers can be added during object creation
  // ...
  HandleUncaughtErrorHandler get handleUncaughtError;
  RunHandler get run;
  RunUnaryHandler get runUnary;
  RunBinaryHandler get runBinary;
  RegisterCallbackHandler get registerCallback;
  RegisterUnaryCallbackHandler get registerUnaryCallback;
  RegisterBinaryCallbackHandler get registerBinaryCallback;
  ErrorCallbackHandler get errorCallback;
  ScheduleMicrotaskHandler get scheduleMicrotask;
  CreateTimerHandler get createTimer;
  CreatePeriodicTimerHandler get createPeriodicTimer;
  PrintHandler get print;
  ForkHandler get fork;
}


, , . โ€” -. , .



โ€” , - :



// This is the actual type of run handler
typedef RunHandler = R Function<R>(
  Zone self, // Reference to the zone with this specification
  ZoneDelegate parent, // Object for delegating work to [self] parent zone
  Zone zone, // On this zone .run() method was initially called
  R Function() action, // The actual work we want to run in [zone]
);

int _counter = 0;

final zone = Zone.current.fork(
  specification: ZoneSpecification(
    // This will be called within [zone.run(doWork);]
    run: <R>(self, parent, zone, action) {
      // RunHandler
      // Delegate an updated work to parent, so in addition
      // to the work being done, the counter will also increase
      parent.run(zone, () {
        _counter += 1;
        action();
      });
    },
  ),
);

void main() {
  zone.run(doWork);
}


. run , run. - โ€” , .



.



, . , -. .



. - , ยซยป . , , , , root . , .



โ€” , .



โ€” , ยซยป . , root , _current .



โ€” , . , , , . , .



, . :





: , , D B



, .



3. - FakeAsync



FakeAsync. , run , . :



class FakeAsync {
  // ...
  ZoneSpecification get _zoneSpec => ZoneSpecification(
        // ...
        scheduleMicrotask: (_, __, ___, Function microtask) {
          _microtasks.add(microtask); // Just save callback
        },
        createTimer: (_, __, ___, Duration duration, Function callback) {
          // Save timer that can immediately provide its callback to us
          var timer = _FakeTimer._(duration, callback, isPeriodic, this);
          _timers.add(timer);
          return timer;
        },
      );
}


scheduleMicrotask. , - , . , Future , Future . : Stream .



FakeAsync c โ€” .



createTimer. createTimer, , , Timer. : ยซ ?ยป. :



abstract class Timer {
  factory Timer(Duration duration, void callback()) {
    // Create timer with current zone
    return Zone.current
        .createTimer(duration, Zone.current.bindCallbackGuarded(callback));
  }
  // ...
  // Create timer from environment
  external static Timer _createTimer(Duration duration, void callback());
}

class _RootZone implements Zone {
  // ...
  Timer createTimer(Duration duration, void f()) {
    return Timer._createTimer(duration, f);
  }
}


โ€” , . , , . FakeAsync: _FakeTimer, โ€” .



class FakeAsync {
  // ...
  ZoneSpecification get _zoneSpec => ZoneSpecification(
        // ...
        scheduleMicrotask: (_, __, ___, Function microtask) {
          _microtasks.add(microtask); // Just save callback
        },
        createTimer: (_, __, ___, Duration duration, Function callback) {
          // Save timer that can immediately provide its callback to us
          var timer = _FakeTimer._(duration, callback, isPeriodic, this);
          _timers.add(timer);
          return timer;
        },
      );
}


run, . , . , . FakeAsync โ€” , .



, ! flushTimers:



class FakeAsync {
  // ...
  void flushTimers() {
    // Call timer callback for every saved timer
    while (_timers.isNotEmpty) {
      final timer = _timers.removeFirst();
      timer._callback(timer);
      // Call every microtask after processing each timer
      _drainMicrotasks();
    }
  }

  void _drainMicrotasks() {
    while (_microtasks.isNotEmpty) {
      final microtask = _microtasks.removeFirst();
      microtask();
    }
  }
}


, , , . , !



, , ZoneSpecification. .



:



  • (handleUncaughtError, errorCallback);
  • (registerCallback*);
  • (createPeriodicTimer);
  • (print);
  • (fork).


, . , , . AngularDart โ€” , .



!




All Articles