With the rapid progress in the world of mobile app development, Flutter has emerged as a real game changer. Created by Google, Flutter is an open-source UI software development kit that aids in developing natively compiled applications for mobile, web, and desktop from a single codebase.
Among its vast collection of features, the topic of our focus today is the "Flutter Stream". Streams play a supervisory role in developing top-notch Flutter applications. They assist in managing asynchronous data and help in reducing the complexity of code that deals with sequences of data.
Programming languages with the ability to evaluate code in an asynchronous manner are an essential part of any developer's toolkit. Dart, being one such language, has various elements that come into play when you want to handle asynchronous data. Among these, one of the main classes is "stream". A stream is a sequence of asynchronous events in Flutter. It is similar to an asynchronous Iterable in that, instead of returning the next event when requested, the stream notifies you when an asynchronous event is ready.
To further understand the stream's role in Flutter, we must shed some light on asynchronous programming and the two main classes that characterize it in Dart – Future and Stream.
A Future represents an object that results from computation that isn't completed immediately. Functions usually return an instance of a Future when they need to perform operations such as I/O, which may take some time. The result is not immediately available, but it will be at some point in the future. This "Future" object can be attached with methods such as "then" or "catchError", to queue up what should happen when the Future completes.
Now, let's talk about Streams. We can think of a Stream as a pipe where data flows over time. We can listen to a Stream and do something each time a piece of data arrives. Stream bundles together this entire operation, and this is precisely how a Flutter Stream works.
When you're working with "Streams", no matter how they're created, they can all be used in the same way: the asynchronous for loop, aka "await for", iterates over the events of a stream like a traditional for loop does over an Iterable.
Let's illustrate this with an example. Consider you are trying to get a sum of a sequence of integer events from a Stream. The function could be written like this:
1 Future<int> sumStream(Stream<int> stream) async { 2 var sum = 0; 3 await for (var value in stream) { 4 sum += value; 5 } 6 return sum; 7 } 8
In this example, we listen to a stream of int data. All data events that come from the stream add up together. The function is marked with the async keyword, which is necessary when using the await for loop.
We can put our code to the test by creating a simple stream of numbers with an async* function.
In the real world, errors and exceptions can occur during the execution of your Flutter application, especially when dealing with streams that fetch data from an external source, like a file from a remote server, so the chances of encountering an error increase. This is where error handling becomes crucial during the use of stream in Flutter.
Streams in Dart are designed to allow them to deliver error events just like they deliver data events. If an error event occurs, such a stream will stop unless designed to give more than one error or provide more data after an error event.
Let's look at how to handle error events using await for. When reading a stream using await for, the loop statement throws the error, which ends the loop. Errors can be caught and processed using try-catch.
Here's how to catch an error that arises when the loop iterator equals to 4:
1 import 'dart:async'; 2 3 Stream<int> countStream(int to) async* { 4 for (int i = 1; i <= to; i++) { 5 if (i == 4) { 6 throw Exception('Intentional exception'); 7 } else { 8 yield i; 9 } 10 } 11 } 12 13 Future<void> main() async { 14 final stream = countStream(5); 15 try { 16 await for (var value in stream) { 17 print(value); 18 } 19 } catch (e) { 20 print('Caught error: $e'); 21 } 22 } 23
In the above example, when the stream hits a snag and starts to throw an error, it ends. i.e., It stops sending events. A proper error message is printed out, indicating what went wrong.
One of the notable aspects of the stream class in Dart is its helper methods, which are engineered to execute frequent operations on a stream. To say, you can extract the last positive integer from a stream. This task can be done using the lastWhere() method available from the Stream API, as in the following code:
1 Future<int> lastPositive(Stream<int> stream) => stream.lastWhere((x) => x >= 0); 2
The helper methods of the Stream class provide us with an interface similar to the methods we've been using with Iterable. These methods become handy during the process of stream manipulation and management.
Diving deeper into Flutter streams, you'll discover two streams: single subscription and broadcast. As their names suggest, they each have special characteristics suitable for different scenarios.
The most common type of stream is a single subscription stream. Such a stream is most useful when we have a sequence of data events that are pieces of a larger whole. Here, the events need to be delivered correctly over a period. If we try to listen to this type of stream more than once, we may lose out on the initial events, rendering the rest of the stream data useless. Consider the scenario of reading a file or receiving a web request. You obviously would not want to miss out on any data. This is precisely where the single subscription stream comes in handy.
Broadcast streams, on the other hand, can handle multiple listeners simultaneously, as opposed to a single subscription stream allowing only a single listener. Broadcast streams are designed for individual messages that can be dealt one at a time, such as the case of event broadcasting, where the same event is broadcast to multiple listeners simultaneously.
So, using a single subscription stream or a broadcast stream completely depends on your app’s specific needs and requirements.
The Stream class in Dart is packed with a number of methods that come in handy while dealing with streams in Flutter. As a Flutter developer, these methods will be your best ally in performing different operations on the streams. The methods can process the stream and generate a result.
A sample list of the main methods you’d find packed in Stream<T>
include:
Future<T>
get first;Future<bool>
get isEmpty;Future<T>
get last;Now, how do we use them? Let’s look at an example. Say, we want to get the first positive integer from a stream, We can use the 'firstWhere' function:
1 Future<int> firstPositive(Stream<int> stream) => stream.firstWhere((x) => x >= 0); 2
We can use more methods to modify the original stream and create a new stream. These methods wait till someone listens to the new stream before they start generating events in the original stream. These methods include map, skip, take, where, etc.
There are three more Flutter stream methods that are worthy of our attention. These are asyncExpand, asyncMap, and distinct.
The asyncExpand and asyncMap functions align with expand and map, with one important distinction - they allow their function arguments to be asynchronous.
For illustration's sake, we'll look at an example using asyncMap. Suppose you have a stream that emits events every second, and you need to transform this stream to now emit the result of a Future that completes 2s after each event emission:
1 Stream<int> performAsyncOperation(int value) async* { 2 // Simulate a Future that takes 2 seconds to complete 3 await Future<void>.delayed(const Duration(seconds: 2)); 4 yield value; 5 } 6 7 void main() { 8 // A stream that emits every second 9 final stream = Stream<int>.periodic(const Duration(seconds: 1), (i) => i); 10 11 // Transform the stream to perform an asynchronous operation 12 final transformedStream = stream.asyncMap(performAsyncOperation); 13 14 // Subscribe to the transformed stream 15 transformedStream.listen(print); 16 } 17
While not existing on Iterable, the distinct function could very well have been there. It provides a way to filter out non-distinct items from a stream automatically.
Regarding streams, error handling, and transformations appear as more particular topics. This is because errors can abruptly halt an await-for loop from executing anymore when they reach the loop. When that happens, it's the end of the loop and its stream subscription. It's clear then that there's no way of recovering from such a situation.
However, Dart provides us with ways to circumvent such situations by applying transformations to prevent or handle errors before they hit the loop block. Flutter stream has methods like handleError(), timeout(), and transform() to deal with such error scenarios and to transform the contents of the stream.
For instance, you can remove errors from a stream using the handleError() method before applying an await for loop.
1 Stream<S> mapLogErrors<S, T>( 2 Stream<T> stream, 3 S convert(T event), 4 ) async* { 5 var streamWithoutErrors = stream.handleError((e) => log(e)); 6 await for (var event in streamWithoutErrors) { 7 yield convert(event); 8 } 9 } 10
The transform() function is a more generalized "map" for streams. A regular map function asks for one value for each incoming event. But, especially for I/O streams, it might take several incoming events to create an output event. StreamTransformer can deal smoothly with such situations.
Streams come in extremely handy with I/O operations. For instance, if you'd like to read a file and decode it using two transformations, it first decodes the string data from UTF8 format before splitting it into lines. Let's see how such an operation can be achieved with streams:
1 import 'dart:convert'; 2 import 'dart:io'; 3 4 void main(List<String> args) async { 5 var file = File(args[0]); 6 var lines = utf8.decoder 7 .bind(file.openRead()) 8 .transform(const LineSplitter()); 9 await for (var line in lines) { 10 if (!line.startsWith('#')) print(line); 11 } 12 } 13
This shows that streams can be used to create an asynchronous data sequence where you can attach multiple transformations before listening to it.
Subscriptions in Dart are immutable bindings to a stream. The 'listen' is a method that all stream users should come to terms with, as it's a crucial part of interfacing with streams. This is because all other stream functions are fundamentally defined as 'listen'. 'Listen' is the "low-level" method for developing a new stream type. By extending the Stream class and implementing 'listen', all other methods on Stream can be used by calling 'listen' to work.
To illustrate, here's how you create a new Stream type:
1 StreamSubscription<T> listen(void Function(T event)? onData, 2 {Function? onError, void Function()? onDone, bool? cancelOnError}); 3
You can start listening to a stream with 'listen'. Until you do, the stream is essentially a description of the events you're attempting to see. When you begin listening, you will be returned a StreamSubscription object representing the active event-producing stream. This is analogous to how an Iterable is simply a collection of objects, whereas the iterator performs the actual iteration.
There you have it! A dive into the world of Flutter streams. Understanding asynchronous programming with Dart’s Future and Stream classes, how to handle stream data and error events, the various methods available in the Stream class for effective stream manipulation, the single subscription stream vs. broadcast streams comparison, and the indispensable 'listen' method for creating and managing new streams.
Streams are a powerful feature in Flutter, allowing developers to handle asynchronous data efficiently. With the examples and explanations in this blog post, you should now be more confident in working with streams and implementing them in your Flutter applications. The ability to handle asynchronous events is now a tool you can use to build yet more effective and efficient Flutter apps.
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.