Advanced Flutter REST Integration
Advanced Asynchronous Programming
Handling Multiple Futures
You already know how to await a single future to get its result. But what if you need to run several asynchronous operations at once and only proceed when they're all finished? For example, you might need to fetch a user's profile, their list of friends, and their recent posts from different API endpoints.
Waiting for each one to complete in sequence is inefficient. The second request would only start after the first one finishes, and the third only after the second. This creates a slow, unnecessary waterfall.
Instead, you can start all the operations at the same time and wait for them all to complete. Dart's Future.wait is perfect for this. It takes a list of futures and returns a single future that completes when all the futures in the list have completed. The result is a list of their values, in the same order you provided them.
// A function that simulates fetching user data.
Future<String> fetchUserData() async {
await Future.delayed(const Duration(seconds: 2));
return 'User Profile';
}
// A function that simulates fetching user posts.
Future<String> fetchUserPosts() async {
await Future.delayed(const Duration(seconds: 3));
return 'Recent Posts';
}
void main() async {
print('Starting concurrent fetches...');
// Future.wait takes a list of Futures.
final results = await Future.wait([
fetchUserData(),
fetchUserPosts(),
]);
// The result is a List of the completed Future values.
print('All data fetched!');
print('User Data: ${results[0]}');
print('User Posts: ${results[1]}');
}
Future.waitis your tool for running independent asynchronous tasks in parallel and gathering all their results at once.
Working with Streams
While a Future represents a single value that will be available later, a Stream represents a sequence of asynchronous events. Think of it like a conveyor belt that delivers items one by one over time. You don't get all the items at once; you receive them as they become available.
Streams are common for handling things like user input events, data from a network connection, or file I/O. To get values from a stream, you don't await it directly. Instead, you listen to it.
The most common way to process a stream is with an asynchronous await for loop. This loop will wait for the next event to be emitted by the stream, process it, and then wait for the next one, continuing until the stream is closed.
// A stream that emits a number every second.
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i; // 'yield' sends a value out on the stream.
}
}
void main() async {
print('Listening to the number stream...');
// Use an 'await for' loop to process stream events.
await for (final number in countStream(5)) {
print(number);
}
print('Stream finished.');
}
In the example above, async* marks the function as an asynchronous generator that produces a stream. The yield keyword is used to emit a value. Each time the await for loop runs, it pulls the latest yielded value from the stream.
Robust Error Handling
Asynchronous operations can fail. A network request might time out, or a file might not exist. Your application needs to handle these errors gracefully instead of crashing.
When using async and await, you can use a standard try-catch block, just like with synchronous code. This is the most common and readable way to handle errors.
Future<String> fetchData() async {
await Future.delayed(const Duration(seconds: 1));
// Simulate a network failure.
throw Exception('Failed to fetch data.');
}
void main() async {
try {
final data = await fetchData();
print('Data received: $data');
} catch (e) {
print('An error occurred: $e');
}
}
What happens if one of the futures in Future.wait fails? The entire Future.wait operation fails immediately with the error from that future. The other futures will continue to run, but you won't get their results. You can wrap the call in a try-catch block to handle this.
For streams, you can also use try-catch around an await for loop to catch any errors emitted by the stream.
Stream<int> errorStream() async* {
yield 1;
await Future.delayed(const Duration(seconds: 1));
throw Exception('Something went wrong!');
// This part is never reached.
yield 2;
}
void main() async {
try {
await for (final number in errorStream()) {
print(number);
}
} catch (e) {
print('Caught an error from the stream: $e');
}
}
Time to test your knowledge of these advanced concepts.
What is the primary benefit of using Future.wait for multiple asynchronous operations?
What is the fundamental difference between a Future and a Stream?
Mastering these patterns for concurrency and error handling is essential for building responsive, reliable Dart applications.