FutureProvider
Handling asynchronous data in Riverpod — loading states, error handling, and working with Futures.
FutureProvider
is a type of provider in Riverpod that holds a
Future
.
It's designed specifically for asynchronous operations like:
- Making API calls (HTTP requests)
- Reading from a database
- Fetching data from local storage
-
Any operation that returns a
Future
The key benefit of
FutureProvider
is that it
handles the loading, data, and error states for you
.
Your UI can react to each state appropriately.
✅ When to Use FutureProvider
Use
FutureProvider
when you need to fetch data asynchronously and display it in your UI.
It's perfect for API calls, database reads, and any operation that takes time to complete.
Defining a
FutureProvider
is similar to a regular provider, but the creation function
is marked as
async
and returns a
Future
.
import 'package:flutter_riverpod/flutter_riverpod.dart';
// A FutureProvider that fetches a user from an API
final userProvider = FutureProvider<User>((ref) async {
// This function is called when the provider is first read
final response = await http.get(Uri.parse('https://api.example.com/user'));
return User.fromJson(response.body);
});
// A FutureProvider that fetches a list of posts
final postsProvider = FutureProvider<List<Post>>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/posts'));
return postsFromJson(response.body);
});
🔑 Key Points
-
The creation function is marked with
async -
It returns a
Futureof the desired type - The provider is read-only — you can't modify the data directly
-
To refresh the data, use
ref.refresh(provider)
When you
watch
a
FutureProvider
, you get an
AsyncValue
object
that represents the current state of the asynchronous operation.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers.dart';
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// 👀 Watch the user provider to get the AsyncValue
final userAsync = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text('User Profile')),
body: Center(
child: userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
data: (user) => Text('Hello, ${user.name}!'),
),
),
);
}
}
userProvider
is a
FutureProvider
that
fetches a
User
from an API. The
async
function makes the HTTP request
and returns the parsed user.
ref.watch(userProvider)
returns an
AsyncValue
that represents the current state of the future: loading, error, or data.
.when()
method handles all three states:
loading
shows a progress indicator,
error
shows an error message,
and
data
displays the user's name.
AsyncValue
is a sealed class that represents the three possible states of a
Future
:
final
asyncValue = ref.watch(userProvider);
// Method 1: Using .when() - most concise
asyncValue.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error'
),
data: (user) => Text(user.name),
);
// Method 2: Using .maybeWhen() - with fallback
asyncValue.maybeWhen(
data: (user) => Text(user.name),
orElse: () => Text(
'Loading or error...'
),
);
// Method 3: Using conditionals
if
(asyncValue.isLoading) {
return
CircularProgressIndicator();
}
else
if
(asyncValue.hasError) {
return
Text(
'Error: ${asyncValue.error}'
);
}
else
{
return
Text(asyncValue.value!.name);
}
By default,
FutureProvider
caches the result after the first successful fetch.
To refetch the data (for example, when the user pulls to refresh), use
ref.refresh()
.
class UserScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(
title: Text('User Profile'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
// 🔄 Refresh the data
ref.refresh(userProvider);
},
),
],
),
body: Center(
child: userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => ref.refresh(userProvider),
child: Text('Retry'),
),
],
),
data: (user) => Text('Hello, ${user.name}!'),
),
),
);
}
}
💡 Refresh vs. Watch
- ref.watch() – Reads the provider and listens for changes
- ref.refresh() – Invalidates the cached data and triggers a new fetch
- ref.invalidate() – Marks the data as invalid without refetching (it will refetch on next watch)
Here's a complete example of a weather app that uses
FutureProvider
to fetch weather data
from an API. This demonstrates loading, error, and data states in a real-world scenario.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// 1. Define the data model
class Weather {
final String city;
final double temperature;
final String condition;
Weather({
required this.city,
required this.temperature,
required this.condition,
});
factory Weather.fromJson(Map<String, dynamic> json) {
return Weather(
city: json['name'] ?? 'Unknown',
temperature: (json['main']['temp'] as num).toDouble() - 273.15,
condition: json['weather'][0]['description'] ?? 'Unknown',
);
}
}
// 2. Define the FutureProvider
final weatherProvider = FutureProvider<Weather>((ref) async {
// Fetch weather data from an API
final response = await http.get(
Uri.parse(
'https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_API_KEY',
),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
return Weather.fromJson(data);
} else {
throw Exception('Failed to load weather: ${response.statusCode}');
}
});
// 3. The UI Widget
class WeatherScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final weatherAsync = ref.watch(weatherProvider);
return Scaffold(
appBar: AppBar(title: Text('Weather App')),
body: Padding(
padding: EdgeInsets.all(16),
child: Center(
child: weatherAsync.when(
loading: () => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading weather data...'),
],
),
error: (error, stack) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Error: $error', textAlign: TextAlign.center),
SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(weatherProvider),
child: Text('Retry'),
),
],
),
data: (weather) => Card(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
weather.city,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'${weather.temperature.toStringAsFixed(1)}°C',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w300,
),
),
SizedBox(height: 8),
Text(weather.condition.toUpperCase()),
SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => ref.refresh(weatherProvider),
icon: Icon(Icons.refresh),
label: Text('Refresh'),
),
],
),
),
),
),
),
),
);
}
}
FutureProvider
- Handles asynchronous data
- Built-in loading state
- Built-in error state
- Read-only (cannot modify data)
- Data is cached after fetch
- Perfect for API calls, database reads
StateProvider
- Handles synchronous data
- No loading state
- No error state
- Mutable (can modify data)
- Data is not cached
- Perfect for counters, toggles, form data
If you don't handle the loading state, your UI might show an empty or incorrect state
while the data is being fetched. Always handle all three states of
AsyncValue
.
Use
.when()
or
.maybeWhen()
to handle loading, error, and data states
explicitly.
ref.watch()
should only be used inside the
build()
method.
For event handlers, use
ref.refresh()
or
ref.invalidate()
.
For refresh buttons or retry logic, use
ref.refresh(provider)
to trigger a new fetch.
If an API call fails and you don't handle the error state, your app will show a red screen. Always handle the error state and provide a way for the user to retry.
In the error state, show a friendly error message and a button to retry the operation.
🎯 Key Takeaway
FutureProvider
is the go-to solution for handling asynchronous data in Riverpod.
It handles loading and error states automatically, caches results, and provides a clean
API for refreshing data.
Use it for all your API calls and async operations.