Dart - Isolates, Synchronous And Asynchronous Workflows

Dart Isolate is where all the written code in your Dart program runs. Let's call it a factory. A Dart program begins its execution from the file's main function. And the isolate shares the same name as the main function (main isolate). An isolate is a component specialised in running the code. Dart is a single-threaded language, meaning all operations you code inside your program are executed on a single thread of execution, one at a time, one after the other. Inside our factory main isolate is a roller that represents the single-threading character of Dart. This single thread for execution is called the Mutator Thread.

On the mutator thread are boxes representing our lines of code, user interface components, or retrieved screen taps that will be processed one after the other. Generally, the boxes represent events issued by our app running in the main isolate. The entire line of boxes represent the event queue. Dart always performs synchronous event scanning, which means it reads each line of code in turn rather than jumping from one to the next. The main distinction is that while events can be processed synchronously or asynchronously, they are issued synchronously.

We have a person in charge of processing events in our factory, an Event Loop. The event handler, which determines what to do with each box (or event) that arrives, is the event loop's brain and serves as the operational brain of the system. A stream of events is handled one at a time by the event loop, just in the order in which they are received, and should an event state the delivery of its content later, the event loop will still process the box, place it on a shelf, and then move on to the next box (event), this results in Darts asynchronous support.

Asynchrony is utilised when you know a task will take an undetermined amount of time to be completed, so our entire app isn’t frozen till we get an answer. These could be network requests to an API, storing a file on a device. Anything taking an undetermined amount of time should be tackled asynchronously.

Our factory has a Helper Thread, a trash attendant who specialises in garbage collection. Operations related to the isolate’s welfare are handled by other internal threads like the helper thread.

Aside from the event queue on the mutator thread, there’s another queue called the microtask queue, which is just a way of prioritizing some elements over others. You declare tasks you want to be processed earlier as microtasks, just as you declare events you want to be processed later as future events.

SYNCHRONOUS OPERATIONS

A synchronous operation is a task that needs solving before proceeding to solve another. Dart uses synchronous operations a considerable number of times. We usually split our problems into multiple parts when attempting to solve them in a Dart project. To solve a huge problem, the minute problems have to be fixed in a defined order, highlighted by the order in which you call the functions in your code. Synchronous operations, tasks, or functions usually return a value type or class which are typically single values. The event loop can also process a synchronous task and return multiple values through an iterable collection of the value’s type or class. An iterable collection is much more of an abstract collection, meaning it only generates its items whenever you access one of them.

An iterable function meant to be synchronous has to be mutated as being a synchronous generator by using the sync* keyword. In a synchronous generator, using the return keyword will return a single unit type, be it a single value or a list of values. So the yield keyword is used to be able to generate values out of the function.

A synchronous generator function can generate null elements since the yield statement might not have been called, while a normal function will always have to return a value.

Two Major Peculiarities Between An Iterable And a List

  1. Iterables are lazy-loaded, meaning the generator function will run only when a potentially generated element from it is accessed.

  2. An iterable generates just the right amount of elements it needs. Always use an iterable when working with synchronously generated functions since they’re smart and efficient.

ASYNCHRONOUS WORKFLOWS

Futures are asynchronous events in which the timing of their solution is uncertain. Let’s assume an asynchronous event denoted by Future is a box, a box that can have four states during its lifetime;

  1. Unprocessed - meaning the box waits in the event or microtask queue to be processed by the event loop.

  2. Uncompleted - the box has been processed by the event loop but has not yet retrieved the expected value.

  3. Completed with value - the box has been processed and completed with a value of the expected type.

  4. Completed with Error - the promised value didn’t arrive, and an exception error is thrown.

SOME FUTURE CONSTRUCTORS

  • Default Future - marks an event as asynchronous and to be solved in the future.

  • Future.delayed - similar to the default constructor, but its computation will be executed after the amount of time you set as the duration parameter. A future delayed with duration 0 is the same as a default future constructor. Keep in mind that the default and delayed futures are set as events on the events queue inside the isolate.

  • Future.value - this constructor doesn’t accept a function as a parameter but rather a normal future value. It’s used whenever you want to complete a future with a value immediately, i.e., print a value directly but in an asynchronous manner. In such instances, the event is placed on the microtask queue because it has the priority of being printed immediately, which is done whenever the value passed as a parameter is completed.

  • Future.async - this is similar to the future.value constructor, the difference being future.async takes a function instead of taking a value as a parameter.

  • Future.microtask - places its events on the microtask queue instead of the event queue.

STREAMS

Similar to an iterable, streams can return null, one or multiple values. The only difference is that stream values are not returned synchronously right away. For the values of a stream to be accessed, they have to be listened to using the listen function. By using the fromFuture constructor, one can create a stream from a bunch of futures in a stream.