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 aString
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;} ())
orFuture<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
andFutureBuilder
.Applying
StreamSubscription
listeners to handle real-time events.Transforming from one stream datatype to another.
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.