If you've started building Flutter apps, you've probably seen widgets like FutureBuilder and StreamBuilder. At first, they can seem confusing, but they're essential for working with data that doesn't arrive immediately, such as API responses, databases, or real-time updates.
In this tutorial, you'll learn what these widgets do, when to use each one, and how to build simple examples step by step.
What Are FutureBuilder and StreamBuilder?
Both widgets help Flutter build the UI based on asynchronous data.
The difference is simple:
- FutureBuilder waits for one result and then stops.
- StreamBuilder listens for multiple results over time.
Think of it like this:
Future
Request → Waiting → One Result → Finished
Stream
Listening → Result 1 → Result 2 → Result 3 → ...
When Should You Use FutureBuilder?
Use FutureBuilder when you only need data once.
Examples include:
- Loading user information
- Fetching products from an API
- Reading a file
- Loading settings from local storage
When Should You Use StreamBuilder?
Use StreamBuilder when data changes continuously.
Examples include:
- Chat messages
- Live stock prices
- GPS location updates
- Firebase Firestore
- Sensor data
Part 1: Understanding FutureBuilder
Step 1: Create a New Flutter Project
flutter create future_builder_demo
Open the project in your favorite editor.
Step 2: Create a Function That Returns Data
Let's pretend we're loading user data from the internet.
Future<String> fetchUser() async {
await Future.delayed(
const Duration(seconds: 3),
);
return "Welcome John!";
}
Here:
-
Future.delayed()simulates a network request. - The function returns data after 3 seconds.
Step 3: Display the Result with FutureBuilder
FutureBuilder<String>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Text(snapshot.data!);
},
)
Let's understand each part.
future
future: fetchUser()
This is the task Flutter should wait for.
builder
The builder is called every time the Future changes state.
builder: (context, snapshot)
The snapshot contains information about the Future.
Step 4: Show a Loading Indicator
While waiting:
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
The user sees a loading spinner.
Step 5: Handle Errors
Sometimes requests fail.
if (snapshot.hasError) {
return Text(
snapshot.error.toString(),
);
}
Always handle errors so your app doesn't crash.
Step 6: Display the Data
When everything succeeds:
return Text(snapshot.data!);
Output
Welcome John!
Complete FutureBuilder Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Future<String> fetchUser() async {
await Future.delayed(
const Duration(seconds: 3),
);
return "Welcome John!";
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("FutureBuilder Demo"),
),
body: Center(
child: FutureBuilder<String>(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
return Text(
snapshot.data!,
style: const TextStyle(fontSize: 22),
);
},
),
),
),
);
}
}
Run the app and you'll see a loading spinner for 3 seconds before the welcome message appears.
Understanding the Snapshot
The snapshot tells you everything about the Future.
Is it still loading?
snapshot.connectionState ==
ConnectionState.waiting
Did it finish?
snapshot.connectionState ==
ConnectionState.done
Was there an error?
snapshot.hasError
Is there data?
snapshot.hasData
Get the data
snapshot.data
Part 2: Understanding StreamBuilder
Unlike a Future, a Stream keeps sending new data.
Imagine a digital clock.
Every second, the time changes.
A StreamBuilder automatically rebuilds the UI whenever new data arrives.
Step 1: Create a Stream
Stream<int> counter() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(
const Duration(seconds: 1),
);
yield i;
}
}
Notice the keyword:
yield
Instead of returning one value, it sends many values.
Step 2: Display the Stream
StreamBuilder<int>(
stream: counter(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Text(
snapshot.data.toString(),
);
},
)
Step 3: Run the App
The screen updates automatically.
1
2
3
4
5
...
10
No setState() is required.
Complete StreamBuilder Example
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Stream<int> counter() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(
const Duration(seconds: 1),
);
yield i;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("StreamBuilder Demo"),
),
body: Center(
child: StreamBuilder<int>(
stream: counter(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Text(
snapshot.data.toString(),
style: const TextStyle(fontSize: 40),
);
},
),
),
),
);
}
}
FutureBuilder vs StreamBuilder
| Feature | FutureBuilder | StreamBuilder |
|---|---|---|
| Receives one value | ✅ | ❌ |
| Receives multiple values | ❌ | ✅ |
| Stops automatically | ✅ | ❌ |
| Good for API calls | ✅ | ❌ |
| Good for live updates | ❌ | ✅ |
| Good for chat apps | ❌ | ✅ |
| Good for Firebase | ❌ | ✅ |
Common Mistakes
Calling the Future Inside build()
Avoid creating a new Future every time the widget rebuilds.
❌ Don't do this:
FutureBuilder(
future: fetchUser(),
)
If the widget rebuilds frequently, fetchUser() may run multiple times.
A better approach is to create the Future once in initState() and reuse it.
Ignoring Errors
Always check:
snapshot.hasError
This makes your app more reliable and helps with debugging.
Forgetting Loading States
Without a loading state, users may see a blank screen while waiting for data.
Always show something like:
const CircularProgressIndicator()
Real-World Examples
Use FutureBuilder for:
- Loading products from a REST API
- Reading local files
- Loading user profiles
- Retrieving Hive or SQLite data once
Use StreamBuilder for:
- Firebase Firestore documents
- Live chat messages
- GPS tracking
- Download progress
- Live notifications
Final Thoughts
FutureBuilder and StreamBuilder are fundamental Flutter widgets that make it easy to work with asynchronous data.
Remember this simple rule:
- FutureBuilder → one-time data.
- StreamBuilder → continuous data.
Mastering these widgets will help you build responsive apps that display loading indicators, handle errors gracefully, and update the UI automatically as data changes.
Top comments (0)