FutureProvider is perfect for asynchronous operations like API calls, database queries, and file operations. It automatically handles loading states, errors, and provides reactive updates.
FutureCreate a FutureProvider with async functions:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
part 'users.g.dart';
@riverpod
Future<List<User>> userList(UserListRef ref) 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');
}
}Use the .when() method to handle different states:
class UserListWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userListProvider);
return usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
leading: CircleAvatar(child: Text(user.name[0])),
);
},
),
loading: () => Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text('Error: $error'),
ElevatedButton(
onPressed: () => ref.invalidate(userListProvider),
child: Text('Retry'),
),
],
),
),
);
}
}Use family providers for parameterized async operations:
@riverpod
Future<User> user(UserRef ref, int userId) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/$userId'),
);
if (response.statusCode == 200) {
return User.fromJson(json.decode(response.body));
} else {
throw Exception('User not found');
}
}
// Usage in widget
class UserDetailPage extends ConsumerWidget {
final int userId;
UserDetailPage({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return Scaffold(
appBar: AppBar(title: Text('User Details')),
body: userAsync.when(
data: (user) => UserDetailView(user: user),
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorView(error: error),
),
);
}
}Implement proper error handling and retry logic:
@riverpod
Future<WeatherData> weather(WeatherRef ref, String city) async {
final maxRetries = ref.watch(maxRetriesProvider);
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
final response = await http.get(
Uri.parse('https://api.weather.com/weather?city=$city'),
headers: {'Authorization': 'Bearer ${ref.watch(apiKeyProvider)}'},
).timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
return WeatherData.fromJson(json.decode(response.body));
} else if (response.statusCode == 404) {
throw CityNotFoundException(city);
} else {
throw WeatherApiException('HTTP ${response.statusCode}');
}
} on TimeoutException {
if (attempt == maxRetries - 1) {
throw WeatherTimeoutException();
}
// Wait before retry
await Future.delayed(Duration(seconds: 2 * (attempt + 1)));
} catch (e) {
if (attempt == maxRetries - 1) rethrow;
await Future.delayed(Duration(seconds: 1));
}
}
throw Exception('Max retries exceeded');
}Control caching behavior and manual refresh:
@riverpod
Future<List<Post>> posts(PostsRef ref) async {
// Cache for 5 minutes
ref.cacheFor(Duration(minutes: 5));
final response = await http.get(
Uri.parse('https://api.example.com/posts'),
);
return (json.decode(response.body) as List)
.map((json) => Post.fromJson(json))
.toList();
}
// In widget - manual refresh
ElevatedButton(
onPressed: () {
ref.invalidate(postsProvider);
},
child: Text('Refresh Posts'),
)Chain FutureProviders together:
@riverpod
Future<UserProfile> currentUser(CurrentUserRef ref) async {
final userId = ref.watch(authProvider).userId;
if (userId == null) throw Exception('User not authenticated');
return ref.watch(userProvider(userId).future);
}
@riverpod
Future<List<Post>> userPosts(UserPostsRef ref) async {
final user = await ref.watch(currentUserProvider.future);
final response = await http.get(
Uri.parse('https://api.example.com/users/${user.id}/posts'),
);
return (json.decode(response.body) as List)
.map((json) => Post.fromJson(json))
.toList();
}cacheForFor data that changes over time, consider using StreamProvider instead. For mutable state with async operations, check out AsyncNotifierProvider.