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.