Flutter: Asynchronous Listing with FutureBuilder
personAhmet Balaman
calendar_today
FlutterFutureBuilderAsyncAwaitWidgetAPI
FutureBuilder is a special structure designed in Flutter to use asynchronous operations within widgets. Since widgets don't directly have async capability, FutureBuilder is required to use asynchronous functions in the UI.
Why FutureBuilder?
- When using functions with async capability, we use await to make it work only until it completes its task
- However, to use await, we need to be inside a function with async capability
- When we want to use an async function inside a widget, it needs to have async capability
- Widgets don't have this capability!
- FutureBuilder structure is required to use async capability within widgets
Live Demo
You can try this widget in the interactive example below:
💡 If the example above doesn't load, click DartPad to run it in a new tab.
Basic Usage
FutureBuilder<String>(
future: fetchData(), // async function
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else if (snapshot.hasData) {
return Text('Data: ${snapshot.data}');
}
return Text('No data found');
},
)
// Async function
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Hello Flutter!';
}Important Properties
| Property | Description |
|---|---|
future |
The Future object to wait for |
builder |
Function that builds the UI |
initialData |
Initial data value |
ConnectionState States
| State | Description |
|---|---|
none |
Future not yet assigned |
waiting |
Future is running, waiting |
active |
Active data stream for Stream |
done |
Future completed |
FutureBuilder with ListView
Fetching data from API and creating a list:
class MyListPage extends StatelessWidget {
Future<List<String>> fetchItems() async {
// API call simulation
await Future.delayed(Duration(seconds: 2));
return ['Flutter', 'Dart', 'Firebase', 'Android', 'iOS'];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('List')),
body: FutureBuilder<List<String>>(
future: fetchItems(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No data found'));
}
final items = snapshot.data!;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index]),
);
},
);
},
),
);
}
}FutureBuilder with GridView
We can use the same approach with GridView.builder:
class MyGridPage extends StatelessWidget {
Future<List<Map<String, dynamic>>> fetchProducts() async {
await Future.delayed(Duration(seconds: 2));
return [
{'name': 'Product 1', 'price': 99, 'color': Colors.red},
{'name': 'Product 2', 'price': 149, 'color': Colors.blue},
{'name': 'Product 3', 'price': 199, 'color': Colors.green},
{'name': 'Product 4', 'price': 249, 'color': Colors.orange},
{'name': 'Product 5', 'price': 79, 'color': Colors.purple},
{'name': 'Product 6', 'price': 129, 'color': Colors.teal},
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: fetchProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No products found'));
}
final products = snapshot.data!;
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
padding: EdgeInsets.all(8),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return Card(
color: product['color'],
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
product['name'],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'\$${product['price']}',
style: TextStyle(color: Colors.white),
),
],
),
),
);
},
);
},
),
);
}
}Fetching Real Data from API
import 'dart:convert';
import 'package:http/http.dart' as http;
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}
class UsersPage extends StatelessWidget {
Future<List<User>> fetchUsers() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Users')),
body: FutureBuilder<List<User>>(
future: fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Error: ${snapshot.error}'),
],
),
);
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
leading: CircleAvatar(
child: Text(user.name[0]),
),
title: Text(user.name),
subtitle: Text(user.email),
trailing: Icon(Icons.chevron_right),
),
);
},
);
},
),
);
}
}FutureBuilder Best Practices
1. Store Future in State
class MyPage extends StatefulWidget {
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
late Future<List<String>> _futureData;
@override
void initState() {
super.initState();
_futureData = fetchData(); // Create Future once
}
Future<List<String>> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return ['Data 1', 'Data 2', 'Data 3'];
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<String>>(
future: _futureData, // Use the same Future
builder: (context, snapshot) {
// ...
},
);
}
}2. Refresh Feature
class _MyPageState extends State<MyPage> {
late Future<List<String>> _futureData;
@override
void initState() {
super.initState();
_loadData();
}
void _loadData() {
setState(() {
_futureData = fetchData();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Data'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadData, // Refresh
),
],
),
body: FutureBuilder<List<String>>(
future: _futureData,
builder: (context, snapshot) {
// ...
},
),
);
}
}Summary
- FutureBuilder: For using async in widgets
- ConnectionState: Waiting, error, completion states
- snapshot.hasData: Data check
- snapshot.hasError: Error check
- ListView.builder + FutureBuilder: Dynamic async list
- GridView.builder + FutureBuilder: Dynamic async grid
FutureBuilder is an indispensable structure for using asynchronous operations like API calls and database operations in Flutter UI.