From Future to Stream: A Practical Guide to Async in Flutter
Welcome back to our beginner-friendly Flutter series! In our last article, State Management with Riverpod: Building a Food Energy Tracker in Flutter, we explored how to manage state in a Flutter app using Riverpod. Today, we’re shifting our focus to one of the most important aspects of modern app development: asynchronous programming.
Asynchronous code is essential in mobile development, where tasks like fetching data from the internet or reading files can take time. In this article, we’ll delve into the basics of asynchronous programming in Dart, including Future
, async
, await
, and Streams
. By the end, you’ll have built a simple app that fetches and displays data from the network.
This series is designed for beginners, and all examples are crafted to run seamlessly in DartPad — an online platform that allows you to code without the need for complex IDE setups. DartPad is a great tool to quickly experiment and learn the fundamentals of Flutter and Dart.
Asynchronous Programming in Dart
Asynchronous programming allows your app to perform time-consuming tasks without freezing the user interface. In Dart, there are two primary ways to handle asynchronous operations: Future
and Stream
.
Future, async, and await
- Future: A
Future
represents a value that will be available at some point in the future. It's like a promise that the result will be delivered later, whether it's data fetched from the internet or the outcome of a long-running computation. - async and await: These keywords simplify working with
Futures
. When you mark a function asasync
, you can useawait
to pause the execution until theFuture
completes. This allows you to write asynchronous code that looks and behaves like synchronous code, making it easier to read and maintain.
Here’s a basic example:
Future<String> fetchData() async {
// Simulating network request delay
await Future.delayed(Duration(seconds: 2));
return 'Data fetched from the network';
}
void main() async {
print('Fetching data...');
String data = await fetchData();
print(data);
}
In this example, the fetchData
function simulates a network request by delaying for 2 seconds before returning a string. The await
keyword ensures that the code waits for fetchData
to complete before moving on, making it clear and easy to understand.
Streams for Handling Data Flows
While Future
is great for handling a single asynchronous operation, Stream
is designed for working with a sequence of asynchronous events. For example, you might use a Stream
to handle continuous data input from a web socket or a file that’s being read line by line.
A Stream
can be listened to, and whenever a new piece of data arrives, your listener is notified and can act on it.
Here’s a simple example of using a Stream
:
Stream<int> countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() {
Stream<int> stream = countStream();
stream.listen((value) {
print('Received: $value');
});
}
In this example, countStream
is a Stream
that emits a sequence of integers with a delay of one second between each. The listen
method is used to handle each value as it arrives.
Building a Data Fetching App
Now that you understand the basics of asynchronous programming in Dart, let’s build a simple Flutter app that fetches and displays data from the internet.
Step 1: Setting Up the Project
If you’re using DartPad, you can skip this step as DartPad is ready to go. If you’re working in an IDE like Android Studio or VS Code, make sure to include the necessary dependencies in your pubspec.yaml
file. For our example, we’ll use the http
package to make network requests:
dependencies:
flutter:
sdk: flutter
http: ^1.2.2
Step 2: Creating the App Structure
Let’s start by setting up the basic structure of our app:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Data Fetching App',
theme: ThemeData(primarySwatch: Colors.blue),
home: DataFetchingScreen(),
);
}
}
This code sets up a basic Flutter app with a single screen, DataFetchingScreen
, where we will implement our data fetching logic.
Step 3: Fetching Data from the Network
Next, we’ll implement the logic to fetch data from a network source. For simplicity, we’ll use a public API that returns a list of posts:
class DataFetchingScreen extends StatefulWidget {
@override
_DataFetchingScreenState createState() => _DataFetchingScreenState();
}
class _DataFetchingScreenState extends State<DataFetchingScreen> {
late Future<List<Post>> futurePosts;
@override
void initState() {
super.initState();
futurePosts = fetchPosts();
}
Future<List<Post>> fetchPosts() async {
// Adding a pause to visualize the loading indicator
await Future.delayed(Duration(seconds: 1));
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((post) => Post.fromJson(post)).toList();
} else {
throw Exception('Failed to load posts');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Data Fetching App'),
),
body: Center(
child: FutureBuilder<List<Post>>(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator(color: Colors.red);
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (!snapshot.hasData) {
return Text('No data found');
} else {
return ListView(
children: snapshot.data!.map((post) => ListTile(
title: Text(
post.title,
style: TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(post.body),
)).toList(),
);
}
},
),
),
);
}
}
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'],
title: json['title'],
body: json['body'],
);
}
}
Explanation
- FutureBuilder: This widget allows you to build your UI based on the state of a
Future
. It handles the asynchronous operation of fetching the data and provides different builders depending on whether the data is still loading, has been successfully fetched, or resulted in an error. - http.get: This method sends a GET request to the specified URL. We use it to fetch data from the API.
- json.decode: The
http
package returns the response body as a string, so we need to decode it into a JSON object.
Conclusion
Asynchronous programming is a vital skill in Flutter development. In this article, we’ve covered the basics of working with Future
, async
, await
, and Streams
, and demonstrated how to apply these concepts by building a simple data-fetching app.
Understanding how to manage asynchronous tasks effectively will greatly enhance your ability to build responsive and user-friendly applications. Keep practicing, explore more complex use cases, and try integrating different APIs into your apps.
Remember, every step you take in mastering Flutter and Dart brings you closer to becoming a proficient mobile developer. Don’t hesitate to share your progress, thoughts, and questions in the comments below. Your engagement helps us all learn and grow together. Keep coding, stay curious, and I look forward to seeing what you build next!