Flutter, built on Dart, is a fantastic framework for building fast, beautiful, and responsive applications. At the heart of its performance is asynchronous programming, which allows your app to perform tasks like fetching data or reading files without freezing the user interface. Two keywords, async and async*, are fundamental to this, but they serve different purposes. Let’s break down the distinction.
The Problem: Blocking the UI
Imagine your app needs to download a large image from the internet. If you do this synchronously (meaning, one step after another), your app would freeze while it waits for the download to complete. This is a bad user experience. Asynchronous programming provides a solution by allowing you to start a time-consuming task and then continue with other operations while you wait for the result.
In Dart and Flutter, the primary tools for this are Future and Stream.
async and Future: The Single Value Promise
The async keyword is used to mark a function as asynchronous. It signifies that the function will return a single value at some point in the future. The function’s return type is wrapped in a Future.
What is a Future?
A Future is an object that represents a potential value or error that will be available later. Think of it as a promise that a function will eventually give you a result. It can be in one of two states: uncompleted or completed. When a Future is completed, it either has a value or an error.
How it works:
When you call an async function, it immediately returns an uncompleted Future. The function’s body then starts to execute, and when it encounters an await keyword, it pauses its own execution and lets the rest of the program continue. Once the awaited operation is complete, the function resumes from where it left off.
Here’s a example:
Future<String> fetchUserData() async {
await Future.delayed(Duration(seconds: 2));
return 'User data fetched!';
}
void main() async {
print('Fetching data...');
String data = await fetchUserData();
print(data); // This line waits for the fetchUserData() to complete
print('Done!');
}
In this code, the main function starts by printing “Fetching data…”, then it calls fetchUserData(). The await keyword pauses the main function until fetchUserData() finishes and returns its single Future. The program doesn’t freeze, but the main function’s execution is temporarily suspended at the await call.
async* and Stream: The Continuous Flow
The async* keyword is used for functions that produce a sequence of values over time. It’s often called an asynchronous generator function. A function marked with async* returns a Stream.
What is a Stream?
A Stream is a sequence of asynchronous events. Think of it like a conveyor belt or a pipe where data flows continuously. You can “listen” to a Stream and process each piece of data as it arrives. A Stream can emit zero, one, or multiple values over its lifetime, and it can also emit an error. It’s perfect for things that happen repeatedly, like user input events, real-time data from a server, or a long-running download with progress updates.
How it works:
Instead of returning a single value, an async* function uses the yield keyword to emit a new value to the stream. The function’s execution isn’t halted; it just produces a value and continues.
Here’s a example:
Stream<int> countSeconds(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
print('Starting to count...');
await for (var number in countSeconds(5)) {
print(number);
}
print('Counting is finished!');
}
Here, the countSeconds function produces a new number every second. The main function uses an await for loop to listen to the Stream. Each time a new number is yielded, the loop body executes. The await for loop is what consumes the stream.
When to use which?
Use async when you need a single result from an asynchronous operation. This is the most common use case for API calls, database queries, or any task that will return a single piece of data when it’s done.
Use async* when you need to handle a series of events over time. This is perfect for things like:
- A countdown timer.
- A stock price ticker that updates in real-time.
- An audio player that emits progress updates.
- A search box that emits a new value every time the user types a character.
Understanding the difference between async and async* is crucial for writing efficient, responsive, and robust Flutter applications. Choosing the right tool for the job ensures your app remains performant and provides an excellent user experience. Happy coding! 🚀
For more on Flutter, explore my other posts.