In modern app development, handling asynchronous data efficiently is crucial. Flutter, a popular framework for building cross-platform mobile applications, offers a powerful tool for this purpose: Streams.
In Dart and Flutter, futures and streams are the main tools for handling asynchronous programming. A Future represents a single value that will be provided later, while a Stream is a sequence of values (0 or more) that can be delivered asynchronously at any time. It is essentially a continuous flow of data.
To get data from a stream, you subscribe (or listen) to it. Whenever data is emitted, you can receive and process it according to your app’s logic.
This chapter explores various uses of streams in a Flutter app. You’ll see how to use streams in different scenarios, read and write data to streams, build user interfaces with streams, and implement the BLoC state management pattern. Like futures, streams can produce data or errors, and this article will show you how to handle both.
How To Use Dart Streams
To explain how to use Dart Streams, we’ll create a simple project that changes a screen’s background colour every second. We’ll create a list of five colours, and every second, the background colour of a Container widget filling the whole screen will change.
A Stream of data will emit the colour information. The main screen will listen to this Stream to get the current colour and update the background.
In this article, I’ll walk you through the core parts of an app that utilizes Dart Streams. We’ll create a stream of data and listen (or subscribe) to that stream.
Step 1
In the stream.dart
file, I created a stream of data by adding a method that returns a Stream
of Color
, and I marked the method as async*
:
Stream<Color> getColors() async* {}
Previously, we’ve marked functions as async
(without the asterisk * symbol). In Dart and Flutter, async
is used for futures, while async*
(with the asterisk ) is for streams. The main difference between a stream
and a future
is the number of events returned: a Future
returns just one event, while a Stream
returns zero to many events. Marking a function as async
creates a generator function, which generates a sequence of values (a stream).
So, To return a stream in an async*
method, we use the yield*
statement. You can think of yield*
as a return
statement, but with a crucial difference: yield*
does not end the function.
Step 2
Next, we use Stream.periodic()
to create a Stream that emits events at specified intervals. In our code, the stream emits a value each second. Inside the method within the Stream.periodic
constructor, we use the modulus operator to choose which colour to display based on the number of seconds that have passed since the method was called and return the appropriate colour.
yield* Stream.periodic(const Duration(seconds: 1), (int t) {
int index = t % colors.length;
return colors[index];
});
This code creates the stream
of data.
Step 3
In the main.dart
file, I added the code to listen to the stream with the changeColor
method;
changeColor() async {
await for (var eventColor in colorStream.getColors()) {
setState(() {
bgColor = eventColor;
});
}
}
The core of this method is the await
for command, which is an asynchronous for loop that iterates over the events of a stream. It’s like a regular for loop, but instead of iterating over a set of data (like a list
), it asynchronously listens to each event in a stream. From there, we call the setState
method to update the bgColor
property.
Complete Code
in the main.dart file
import 'package:flutter/material.dart';
import 'package:cookbook/stream.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Color bgColor = Colors.blueGrey;
late ColorStream colorStream;
void changeColor() async {
await for (var eventColor in colorStream.getColors()) {
setState(() {
bgColor = eventColor;
});
}
}
@override
void initState() {
super.initState();
colorStream = ColorStream();
changeColor();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Stream')),
body: Container(
decoration: BoxDecoration(color: bgColor),
),
);
}
}
In the stream.dart file
import 'package:flutter/material.dart';
class ColorStream {
final List<Color> colors = [
Colors.blueGrey,
Colors.amber,
Colors.deepPurple,
Colors.lightBlue,
Colors.teal
];
Stream<Color> getColors() async* {
yield* Stream.periodic(const Duration(seconds: 1), (int t) {
int index = t % colors.length;
return colors[index];
});
}
}
Final UI
Conclusion
There’s more to explore. Instead of using an asynchronous for loop, you can leverage the listen method over a stream. Here’s how:
- Remove or comment out the content of the
changeColor
method in themain.dart
file. - Add the following code to the
changeColor
method:
colorStream.getColors().listen((eventColor) {
setState(() {
bgColor = eventColor; });
});
- Run the app. You’ll notice the app behaves just like before, changing the screen colour each second.
- The main difference between
listen
andawait for
is that withlisten
, execution continues even if there is code after the loop, whileawait for
stops execution until the stream completes.
In this app, we never stop listening to the stream, but it’s important to close a stream when it has completed its tasks. You can use the close()
method for this, as shown in the next recipe.
Streams in Flutter are powerful tools for handling asynchronous data and can be applied in various real-world scenarios, such as real-time messaging, file uploads and downloads, user location tracking, and handling sensor data from devices.
Leave a Reply