FutureProvider

FutureProvider is perfect for asynchronous operations like API calls, database queries, and file operations. It automatically handles loading states, errors, and provides reactive updates.

When to Use FutureProvider

Basic Syntax

Create a FutureProvider with async functions:

users.dart
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');
  }
}

Using FutureProvider in Widgets

Use the .when() method to handle different states:

user_list_widget.dart
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'),
            ),
          ],
        ),
      ),
    );
  }
}

Family Providers

Use family providers for parameterized async operations:

user_detail.dart
@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),
      ),
    );
  }
}

Error Handling

Implement proper error handling and retry logic:

weather.dart
@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');
}

Caching and Refresh

Control caching behavior and manual refresh:

posts.dart
@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'),
)

Dependent Providers

Chain FutureProviders together:

dependent_providers.dart
@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();
}
FutureProvider Loading States Demo
Watch how FutureProvider automatically handles loading, data, and error states

Best Practices

Do

  • Always handle errors appropriately
  • Use timeouts for network requests
  • Implement retry logic for transient failures
  • Use meaningful error types and messages
  • Cache expensive operations with cacheFor

Don't

  • Ignore error states in your UI
  • Make blocking calls in provider functions
  • Create infinite loops with provider dependencies
  • Use FutureProvider for frequently changing data

Need Real-time Updates?

For data that changes over time, consider using StreamProvider instead. For mutable state with async operations, check out AsyncNotifierProvider.