Mastering Concurrency in Flutter: Futures and Streams Explained
In previous article we learned about difference between Future and Stream in flutter.
Now let’s dive a little bit more in Flutter concurrency. First of all, lets wipe the dust around difference between synchronous and asynchronous programming in Flutter:
Table of Contents:
1- Future
2- Stream
2.1- Single Subscription Stream
2.2- Broadcast Stream
2.3- StreamBuilder
3- Conclusion
Synchronous programming means each statement in our code will execute one at a time and after that we’ll move to the next statement.
But in contrast, when we’re using asynchronous programming principals we don’t have such limits. Async programming empowers us to execute multiple statements simultaneously. So, imagine how helpful will be async programming in a scenario that we have to handle multiple requests at once and concurrent operations.
1- Future
First of all, let's take a look at a simple Future implementation. In this code snippet we have a function that returns every string that it gets with 1 second delay to mimicking a server behavior.
//1
Future<String> delayedData(String data) async {
//2
await Future.delayed(const Duration(seconds: 1));
return data;
}
//3
void main() async {
//4
print(await delayedData('A ${DateTime.now()}'));
print(await delayedData('B ${DateTime.now()}'));
print(await delayedData('C ${DateTime.now()}'));
print('1 ${DateTime.now()}');
print(await delayedData('D ${DateTime.now()}'));
}
Output: A 13:05:01
B 13:05:02
C 13:05:03
1 13:05:04
D 12:05:04
Okay, that was a solid start with using Future in the code. Now, let's explore what Stream has to offer us.
2- Stream
Streams can seem more complex than Futures, but this complexity will disappear once you grasp their workings and implementation. Understanding the Stream structure is key to effectively utilizing it in various use cases. Additionally, Streams can be either single-subscription or broadcast types. We'll tear down a straightforward single subscription model and then tackle a more complex broadcast stream.
After that we'll became familiar with
领英推荐
2.1- Single Subscription Streams:
let’s go through the Single Subscription Stream code:
import 'dart:async';
//1
StreamController<String> streamController = StreamController();
//2
Future<void> delayedData(String data) async {
await Future.delayed(const Duration(seconds: 1));
streamController.add(data);
}
void main() async {
//3
streamController.stream.listen((event) {
print(event);
});
//4
await delayedData('A ${DateTime.now()}');
await delayedData('B ${DateTime.now()}');
await delayedData('C ${DateTime.now()}');
await delayedData('D ${DateTime.now()}');
print('1 ${DateTime.now()}');
await delayedData('E ${DateTime.now()}');
//5
streamController.close();
}
output: A 13:05:31
B 13:05:32
1 13:05:34
C 13:05:33
D 13:05:34
The use of StreamController allows for the creation of a custom stream of events that can be listened to and acted upon within the application.
2.2- Broadcast Streams:
The core difference between a single subscription stream and a broadcast stream is that single subscription streams allow only one listener. Adding a new listener will automatically cancel the previous one. This is ideal for streams that produce a limited amount of data, like the result of a network request.
On the other hand, broadcast streams can support an unlimited number of listeners. It's important to note, however, that events added before subscribing will not be received by those who subscribe later. Since events are not buffered for late subscribers, it's vital to add listeners before adding events.
Another difference between single-subscription streams and broadcast streams is the way each one is declared. In the previous example, we became familiar with the declaration of single-subscription streams, which was as follows:
StreamController<String> streamController = StreamController();
However, if a broadcast stream is required, the declaration would be as follows:
StreamController streamController = StreamController.broadcast();
2.3- StreamBuilder
The StreamBuilder is particularly useful in scenarios where your UI needs to update dynamically based on a stream of data, such as chat applications, live feeds, or any other real-time data sources. This is a powerful tool that connects the asynchronous world of streams with your app’s user interface. It listens to a Stream and instructs the Flutter framework to rebuild the widget each time a new event is emitted by the stream.
When you provide a Stream to a StreamBuilder, it automatically subscribes to the stream and starts listening for events. Each time a new event is emitted from the stream, the StreamBuilder gets notified and schedules a rebuild of its widget tree. The StreamBuilder uses the latest snapshot of data from the stream to build its widgets. This snapshot contains the data and also information about the connection state of the stream (like whether it’s currently active, waiting, or done). You can customize the behavior of the StreamBuilder based on the state of the stream. For example, you might show a loading indicator while the stream is in the waiting state and display the actual data when values are received. If the stream emits an error, the StreamBuilder can capture that error and allow you to build a UI response accordingly, such as displaying an error message.
Here is a simple implantation of how StreamBuilder works:
class TestStreamBuilder extends StatelessWidget {
final StreamController<String> streamController = StreamController();
TestStreamBuilder({super.key});
@override
Widget build(BuildContext context) {
addDataToStream();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Stream Builder Example')),
body: StreamBuilder<String>(
stream: streamController.stream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
switch (snapshot.connectionState) {
case ConnectionState.none:
return const Center(child: Text('No connection or null'));
case ConnectionState.waiting:
return const Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Center(child: Text('Data: ${snapshot.data}'));
case ConnectionState.done:
return const Center(child: Text('Stream ended!'));
}
},
),
),
);
}
Future<void> delayedData(String data) async {
await Future.delayed(const Duration(seconds: 1));
streamController.add(data);
}
Future<void> addDataToStream() async {
await delayedData('A');
await delayedData('B');
await delayedData('C');
await delayedData('D');
streamController.close();
}
}
This code snippet is fairly straightforward, and you should be able to understand it from here. Try creating a simple test project to observe the output of this code. However, if you have any questions about StreamBuilder, please feel free to ask.
3- Conclusion
The exploration of Flutter’s concurrency mechanisms reveals the distinct functionalities and applications of Futures and Streams. Futures provide a straightforward way to handle asynchronous operations, allowing the execution of code sequences with unintentional delays. Streams, on the other hand, offer a more complex but powerful approach to managing a sequence of asynchronous events, with the flexibility of single subscription or broadcast models to suit different scenarios. The StreamController plays a crucial role in controlling the flow of data within a Stream. By understanding and implementing these concepts, developers can effectively manage concurrent operations in Flutter, leading to more responsive and efficient applications.
This covers the basics you need to begin implementing Futures and Streams in your code. However, this is not the extent of what you should know.
Continue practicing on your own. Should you encounter any questions or issues, do not hesitate to reach out. Your feedback is always welcome. If you found this article helpful as well as to share it with your network if you found it useful. ??