Futures play an integral part in asynchronous programming in Dart. They return single data or error events asynchronously. Streams function in a similar way, but they deal with sequences of events instead of single events. So, streams are to future objects what iterators are to integers. Streams emit data events (or elements), error events, and “done” events to notify the end of the event flow.
In other words, streams are a source of asynchronous events delivered sequentially. There are data events, which are sometimes referred to as elements of the stream due to a stream’s similarity to a list, and there are error events, which are notifications of failure. Once all data elements have been emitted, a special event signaling the stream is done will notify any listeners that there is no more.
Points to remember:
A stream is like a pipe, you put a value on the one end, and if there’s a listener on the other end that listener will receive that value.
You can process a stream using either await for or listen() from the Stream API.
Important concepts of Streams in Flutter:
Stream Controller: A StreamController simplifies stream management, automatically creating a stream and sink, and providing methods for controlling a stream’s behavior. A StreamController object in Dart does exactly what the name suggests, it controls Dart Streams. The object is used to create streams and send data, errors, and done events on them. Controllers also help check a stream’s properties, such as how many subscribers it has or if it’s paused.
Stream Builders: StreamBuilder is a widget that uses stream operations and basically, it rebuilds its UI when it gets the new values that are passed via Stream it listens to.
StreamBuilder requires 2 parameters:
stream: A method that returns a stream object
builder: widgets that will be returned during different states of a streambuilder.
There are two important points to know –
Sink: In Flutter Streams, a Sink is a point from where we can add data into the stream pipe.
Source: In Flutter Stream, a Source is a point from where we can keep listening to stream data or get the data that is been added into the stream.
EXAMPLE FOR STREAM CONTROLER:-
import 'dart:async';
void main() async {
final streamController = StreamController<DateTime>();
Timer.periodic(Duration(seconds: 2), (timer) {
streamController.add(DateTime.now());
});
streamController.stream.listen((event) {
print(event);
});
}
The output of this is as follows:
2021-10-28 17:56:00.966
2021-10-28 17:56:02.965
2021-10-28 17:56:04.968
2021-10-28 17:56:06.965
2021-10-28 17:56:08.977
2021-10-28 17:56:10.965
Two types of Streams:
Single subscription streams
Broadcast streams
1. Single Subscription Streams:
Single subscription streams are the default. They work well when you’re only using a particular stream on one screen.
A single subscription stream can only be listened to once. It doesn’t start generating events until it has a listener and it stops sending events when the listener stops listening, even if the source of events could still provide more data.
Single subscription streams are useful to download a file or for any single-use operation. For example, a widget can subscribe to a stream to receive updates about a value, like the progress of a download, and update its UI accordingly.
Fortunately, we can keep a reference to our subscription and cancel it when we’re not using it anymore. In this case, we’re canceling our subscription after a certain amount of time:
import 'dart:async';
void main() async {
final streamController = StreamController<DateTime>();
final unsubscribeAt = DateTime.now().add(Duration(seconds: 10));
StreamSubscription<DateTime>? subscription;
Timer.periodic(Duration(seconds: 2), (timer) {
streamController.add(DateTime.now());
});
subscription = streamController.stream.listen((event) async {
print(event);
if (event.isAfter(unsubscribeAt)) {
print("It's after ${unsubscribeAt}, cleaning up the stream");
await subscription?.cancel();
}
});
}
Again, we have our subscription, but now we’re canceling it. When we cancel it, the app can release the resources involved in making the subscription, thus preventing memory leaks within our app.
Cleaning up subscriptions is integral to using streams in Flutter and Dart, and, if we want to use them, we must use them responsibly.
2. Broadcast streams:
If you need multiple parts of your app to access the same stream, use a broadcast stream, instead. A broadcast stream allows any number of listeners. It fires when its events are ready, whether there are listeners or not. To create a broadcast stream, you simply call asBroadcastStream() on an existing single subscription stream.
Syntax: final broadcastStream = singleStream.asBroadcastStream();
some of the useful methods:
add() method: handles forwarding any data to the sink.
addError() method: If an error occurs and your stream’s listeners need to be informed then addError() is used.
listen() method: We listen to the stream for the data incoming with .listen() method.
Conclusion
Streams are a necessary part of handling and processing asynchronous data. It’s possible that the first time you encounter them in any language, they can take quite a bit of getting used to, but once you know how to harness them, they can be very useful.
Flutter also makes it easy for us by way of the StreamBuilder
to rebuild our widgets whenever it detects an update. It’s easy to take this for granted, but it actually takes a lot of the complexity out of the mix for us, which is always a good thing.