Asynchronous Code in Dart: The dart:async Library

At some point during your Dart development process, you might need to call a method without affecting the execution of your code, or call a library method that has been declared as an async method.

That’s when you realize the necessity for asynchronous code and the dart:async built-in Dart library, which allows async functions to return either a Future or a Stream.

The Future

The Future is the most basic object type related to asynchronous methods—it is the return type of all async functions, including void ones:

 Future<​void​> doSomethingThatTakesLong() async {
 // do something
 }
 void​ ​callingSyncFunction​() {
  doSomethingThatTakesLong();
 /* the code won't wait for the function to
  * finish its execution and it will carry on
  * in its regular order, executing
  * the next instruction
  */
 }

If you need to wait for the function to stop executing (perhaps because you need the returned value), you need to choose between using await or .then.

await

await is useful when you actually need to wait for an async function to finish its execution, often because you need its return value. It converts a Future into the value returned by the function by waiting for the code that returned the future to finish its execution:

 Future<​int​> getValue() async {
 return​ 5;
 }
 
 Future<​int​> callingFunction() async {
 var​ n = await getValue();
 return​ n+5;
 }

then(), catchError(), and whenComplete()

If there are some actions you want to take when the Future returns a value, but it doesn’t affect the rest of the current code block’s execution or its return value, you can use the Future’s .then method, which allows you to add a callback function to be called with the Future’s return value when it is available:

 Future<​int​> getValue() async {
 return​ 5;
 }
 
 void​ ​main​() {
  getValue().then((n) {
 assert​(n == 5);
  });
 /* something else that
  * won't wait for getValue
  * to return to execute
  */
 }

What if there’s a runtime error in the execution of the future? That’s when we use catchError:

 void​ ​printValue​() {
  getValue()
  .then(print)
  .catchError((err) {
 // handle error
  });
 }

You can add a whenComplete call (a lot like a finally block) for code that needs to be executed regardless of anything that happens.

Streams and StreamSubscriptions

There are certain kinds of data that are subject to frequent change, like the battery and connectivity status we saw in Chapter 4, Beyond the Standard Library: Plugins and Packages or the documents in the Firebase Cloud Firestore we saw in Chapter 7, Build a Chat App Using Firebase . For this kind of data, we use special asynchronous methods that yield different values at different times or many different values at once.

Creating a Stream

The most basic way to create a Stream is to return multiple values from an asynchronous generator function (async*) using the yield keyword.

yielding Values from a Generator Function

A function that generates a Stream looks something like this:

 Stream<​String​> myStream() async* {
 var​ returnValues = [
 "This is The First String You'll See"​,
 "After 2 Seconds You'll See This"​,
 "Then This"
  ];
 for​ (​int​ i = 0; i < 3; i++) {
  await Future.delayed(Duration(
  seconds: 2
  ));
  yield returnValues[i];
  }
 }

Future.delayed allows you to define a Future that can be used to pause execution (by waiting for it to be done) for a specified amount of time.

yield is just like return, but it doesn’t move the execution outside the asynchronous generator function: it yields a value to the calling function and then resumes execution inside the generator function, allowing it to yield another value later.

Using a StreamController

A StreamController is a more advanced way of generating Streams: it allows you to generate a Stream with a regular synchronous function and in more complex scenarios in which yield just doesn’t cut it.

Once you define a StreamController variable in a function, you can use StreamController.add anywhere within the function (perhaps in a callback) and have an effect similar to yield. Another very important StreamController method is StreamController.close, which causes listening functions to stop listening to the Stream.

The StreamController has two constructors: StreamController (which is the default) and StreamController.broadcast.

The StreamController constructor, which is used for streams that are only supposed to have one listener, takes four callback functions as arguments:

  • onListen, which is fired when a listener starts listening for objects and should trigger the start of the yielding of values to avoid creating memory leaks by firing events that no listener is listening to, causing Dart to buffer them, using up memory for events that may never be listened to anywhere.

  • onPause, which is fired when the listener requests the Stream to temporarily not yield anything using StreamSubscription.pause, for the same reason onListen.

  • onResume, which is fired when the listener requests the Stream to resume the yielding of values after a pause by calling StreamSubscription.resume, it should be obvious at this point that this function should cause the stream to resume firing events.

  • onCancel, which is fired when the listener cancels its subscription by calling StreamSubscription.cancel and is supposed to make the Stream stop trying to yield values.

The StreamController.broadcast constructor, which is used for broadcast streams, can be listened to by multiple listeners. It takes only onListen and onCancel as arguments.

Using a Stream

We’ve seen the basics of how to use Streams in Getting the Battery and Connectivity Data: Asynchronous Programming Using Streams. That example followed this structure:

 class​ MyWidget ​extends​ StatefulWidget {
 // ...
 }
 
 class​ MyState ​extends​ State<MyWidget> {
 // ...
  StreamSubscription<Type> _streamSub;
 
  @override
 void​ initState() {
 // ...
  _streamSub = stream.listen(
  (Type res) {
 // do something with the response, setState()
  }
  );
  }
 
  @override
  Widget build(BuildContext context) {
 // ...
  }
 
  @override
  dispose() {
 // ...
  _streamSub.cancel();
  }
 }

In that case, the stream was provided directly by a library, and we used setState to update some variables when new data was available and rebuild the widget.

All of that is inefficient if the StatefulWidget that is rebuilt every time the stream changes isn’t just the part that’s showing the data from that Stream.

The StreamBuilder

Building a separate StatefulWidget for that isn’t necessary because, just like the FutureBuilder exists to build UI elements based on the results of Future, the StreamBuilder is the Flutter widget that builds its contents based on the data returned by a Stream.

The StreamBuilder’s constructor requires, just like the FutureBuilder, a data type and two arguments, but with a few differences.

The basic structure of a StreamBuilder widget is the following:

 StreamBuilder<Type> {
  stream: stream,
  builder: (context, snapshot) {
 if​(snapshot.hasData)
 // display snapshot.data
 else
 // display an error or some indication that the data is loading
  }
 }

One of the differences between the StreamBuilder and the FutureBuilder lies in the snapshot.connectionState value. When working with a Future in a FutureBuilder, it could only assume the following values:

  • ConnectionState.none if the future argument is set to null (not set at all).

  • ConnectionState.waiting if the FutureBuilder is waiting for the future to return a value.

  • ConnectionState.none if the future has returned a value, meaning snapshot.hasData is true.

This is why we only used snapshot.hasData and not snapshot.connectionState when working with Futures and FutureBuilders. When working with Streams, though, there’s more to it—the connection state could also be ConnectionState.active, meaning that the Stream has already returned a value, but it hasn’t terminated, meaning it could return another value at any time.

snapshot.hasError and the snapshot.error exist for both in case you need to handle errors.

The await for Loop

The Stream can be interacted with in an alternative way to using the listen method: the await for loop.

It’s just like a for-in loop but, instead of iterating over known values in a list, it waits for new values returned by the stream and only terminates when the stream terminates:

 await ​for​(​var​ value ​in​ stream) {
 // do something with the value
 }

For example, we can print to the console the strings returned by the myStream function we defined earlier as soon as it yields them with the following loop:

 await ​for​(​var​ res ​in​ myStream()) {
  print(res);
 }

Instead of printing results the await for can be used, for example, to take an existing Stream, change the data or use it as an argument for some other function, and then yield a changed value, creating a new Stream.

Error Handling Within Streams

When using a StreamBuilder, the snapshot you get in the builder has the same snapshot.hasError and snapshot.error you get with FutureBuilders so you can use those if you need to handle errors within a FutureBuilder or a StreamBuilder

The listen method optionally takes two more callbacks as arguments:

  • onError, which is run when there is an error is thrown by the asynchronous function that generates the Stream.

  • onDone, which is run when the Stream terminates.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset