Asynchronous Programming (Futures and Streams) in Dart

What is Asynchronous Programming?

Asynchronous programming is a programming technique that allows tasks to run concurrently (at the same time) without blocking the execution of other tasks.

What is a Future?

A Future is the result of an asynchronous computation. If you send any API requests in your Flutter application, then you know about async (Future).

What is a Stream?

A stream is a sequence of asynchronous events that can be of any data type. Events might include the following:

  • Data

  • Error

  • Completion Events

Now that that’s out of the way let’s get into it shall we?

The keyword here is Events, unlike a Future which returns a single event (response), a Stream can return several events (responses).

Think of it this way, a future returns a single event, while a stream contains more than one future to give more than one event.

If you are anything like me, a code snippet example would be much better at explaining these concepts

Future

import "dart:async";
void main() async {
  String val = await helloString(value: 'Test value');
  print(val);
}
Future<String> helloString({required String value}) async {
  await Future.delayed(Duration(seconds: 5));
  return value;
}

Let’s break down this code snippet.

Before performing any asynchronous programming, we need to import the dart async library.

  • helloString: An asynchronous function that returns a String value after 5 seconds.

  • async: A keyword used make a function asynchronous.

  • await: A keyword that lets a function pause until an asynchronous task completes.

Stream

There are four types of streams.

  • Single Subscription: A single-subscription stream can only be listened to by one listener at a time. If another listener tries to subscribe, it will throw an error.

  • Broadcast: There could be an infinite number of the listeners to this stream.

  • Periodic Stream: A stream that emits values at regular intervals. This is a subtype of broadcast streams, typically used for time-based events.

  • StreamController: A StreamController allows you to create custom streams and manually add events to them. It can create both single-subscription and broadcast streams.

Now that we have covered the type of streams, let’s look into stream methods.

  • Stream<T>.periodic

      import "dart:async";
      void main() async {
        Duration duration = Duration(seconds: 2);
        Stream<int> stream = Stream.periodic(
          duration,
          (computationCount) {
            return computationCount + 1;
          },
        );
        await for (int i in stream) {
          print(i);
        }
      }
    

    Let’s break down this code snippet.

    Just like with Future, we import the dart library dart:async to access asynchronous classes.

    Stream<T>.periodic creates a stream that repeatedly emits events at periodic intervals. If the callback computationCount is omitted the event values will all be null.

    • We specified a duration of 2 seconds, so the stream returns data every 2 seconds.

    • The argument to this callback is an integer that starts with 0 and is incremented for every event by a factor of 1.

    • We did not specify a termination condition, so the stream runs infinitely.

  • Stream<T>.take(int count)

      import "dart:async";
      void main() async {
        Duration duration = Duration(seconds: 2);
        Stream<int> stream = Stream.periodic(
          duration,
          (computationCount) {
            return computationCount + 1;
          },
        ).take(5);
        await for (int i in stream) {
          print(i);
        }
      }
    

    The take method is used to limit the values returned by the stream. In this code snippet, any value higher than 5 will terminate the stream function.

  • Stream<T>.takeWhile(bool test(T element))

      import "dart:async";
      void main() async {
        Duration duration = Duration(seconds: 2);
        Stream<int> stream = Stream.periodic(
          duration,
          (computationCount) {
            return computationCount + 1;
          },
        );
        stream = stream.takeWhile(
          (element) {
            return element <= 4;
          },
        );
        await for (int i in stream) {
          print(i);
        }
      }
    

    The takeWhile method is used to test for conditions on the stream. In this code snippet, any value higher than 4 will terminate the stream function.

  • Stream<T>.toList()

      import "dart:async";
      void main() async {
        Duration duration = Duration(seconds: 2);
        Stream<int> stream = Stream.periodic(
          duration,
          (computationCount) {
            return computationCount + 1;
          },
        ).take(10);
        List<int> data = await stream.toList();
        for (int i in data) {
          print(i);
        }
      }
    

    This converts the values returned by the stream to a list.

    Note that the take(int count) method was placed as an extra method, if the limit isn’t specified, the stream function will run infinitely.

  • Stream<T>.value

      import "dart:async";
      void main() async {
        printSingleData(Stream.value('Data'));
      }
      Future<void> printSingleData(Stream<String> data) async {
        await for (var x in data) {
          print(x);
        }
      }
    

    This stream returns a single data event of type String and then closes with a done event.

    The returned stream is effectively equivalent to one created by (() async* {yield value;} ()) or Future<T>.value(value).asStream().

      import "dart:async";
      void main() async {
        Stream<int> stream() async* {
          yield 1;
        }
        await for (int i in stream()) {
          print(i);
        }
      }
    
      import "dart:async";
      void main() async {
        Future<int> intFuture() async {
          return 1;
        }
        final stream = intFuture().asStream();
        await for (int i in stream) {
          print(i);
        }
      }
    

Conclusion

There is still a lot more to cover about asynchronous programming, streams and futures. By learning the basics, how and when to use them, you can improve your programming skills as a Flutter/Dart developer or engineer.

Next Step

  • Using asynchronous builder widgets such as StreamBuilder and FutureBuilder.

  • Applying StreamSubscription listeners to handle real-time events.

  • Transforming from one stream datatype to another.

0
Subscribe to my newsletter

Read articles from Ebinehita Sylvester-Paul directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ebinehita Sylvester-Paul
Ebinehita Sylvester-Paul

Hello 👋, I am a Software Engineer (Flutter). I craft and create software solutions which are both rich in user experience, user interactions and functionality. I would be honored to connect with like-minded people who strive to be 1% better every day in this rapidly developing world of technology and software.