AsyncValue
Handling loading, error, and data states elegantly in Riverpod with type-safe pattern matching.
AsyncValue
is a
sealed class
in Riverpod that represents the state of an
asynchronous operation. It elegantly wraps the three possible states of a
Future
or
Stream
:
✅ Why AsyncValue?
AsyncValue
provides a
type-safe
way to handle asynchronous states.
Instead of using
null
checks or custom state classes, you get a unified API that
guarantees you handle all three states.
AsyncValue
can be in one of three states. Each state provides different properties:
// 1. LOADING - The operation is in progress
final
loadingValue = AsyncValue<User>.loading();
// Properties: isLoading = true, hasValue = false, hasError = false
// 2. ERROR - The operation failed
final
errorValue = AsyncValue<User>.error(
Exception(
'Failed to fetch user'
),
StackTrace.current,
);
// Properties: hasError = true, error = Exception, stackTrace = ...
// 3. DATA - The operation completed successfully
final
dataValue = AsyncValue<User>.data(User(name:
'Alice'
));
// Properties: hasValue = true, value = User, isLoading = false
Riverpod provides several ways to handle
AsyncValue
states. The most common is the
.when()
method.
// 1. .when() - Handle all three states explicitly
asyncValue.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error'
),
data: (user) => Text(
'Hello, ${user.name}!'
),
);
// 2. .maybeWhen() - Handle specific states with a fallback
asyncValue.maybeWhen(
data: (user) => Text(
'Hello, ${user.name}!'
),
orElse: () => Text(
'Loading or error...'
),
);
// 3. .whenOrNull() - Handle specific states, return null for others
asyncValue.whenOrNull(
data: (user) => Text(
'Hello, ${user.name}!'
),
);
// 4. Conditional checks
if
(asyncValue.isLoading) {
return
CircularProgressIndicator();
}
else
if
(asyncValue.hasError) {
return
Text(
'Error: ${asyncValue.error}'
);
}
else
if
(asyncValue.hasValue) {
return
Text(
'Hello, ${asyncValue.value!.name}!'
);
}
💡 Which Method to Use?
-
Use
.when()when you need to handle all three states -
Use
.maybeWhen()when you have a fallback for unhandled states -
Use
.whenOrNull()when you only care about the data state - Use conditionals when you need more complex logic
Here's a complete example showing how to use
AsyncValue
to display a user profile
with loading, error, and data states.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
class
User
{
final
String id;
final
String name;
final
String email;
User({
required
this
.id,
required
this
.name,
required
this
.email});
}
final
userProvider = FutureProvider<User>((ref) async {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
// Return user data
return
User(
id:
'1'
,
name:
'Alice Johnson'
,
email:
'alice@example.com'
,
);
});
import
'package:flutter/material.dart'
;
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
import
'user_provider.dart'
;
class
UserScreen
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
final
userAsync = ref.watch(userProvider);
return
Scaffold(
appBar: AppBar(title: Text(
'User Profile'
)),
body: Padding(
padding: EdgeInsets.all(
16
),
child: Center(
child: userAsync.when(
loading: () => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height:
16
),
Text(
'Loading user profile...'
),
],
),
error: (error, stack) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size:
64
, color: Colors.red),
SizedBox(height:
16
),
Text(
'Failed to load user'
,
style: TextStyle(fontSize:
18
, fontWeight: FontWeight.bold),
),
SizedBox(height:
8
),
Text(
error.toString(),
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
SizedBox(height:
16
),
ElevatedButton(
onPressed: () => ref.refresh(userProvider),
child: Text(
'Retry'
),
),
],
),
data: (user) => Card(
child: Padding(
padding: EdgeInsets.all(
24
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius:
50
,
child: Text(
user.name.split(
' '
).map((e) => e[
0
]).join(),
style: TextStyle(fontSize:
24
),
),
),
SizedBox(height:
16
),
Text(
user.name,
style: TextStyle(fontSize:
24
, fontWeight: FontWeight.bold),
),
SizedBox(height:
8
),
Text(
user.email,
style: TextStyle(fontSize:
16
, color: Colors.grey),
),
SizedBox(height:
16
),
ElevatedButton.icon(
onPressed: () => ref.refresh(userProvider),
icon: Icon(Icons.refresh),
label: Text(
'Refresh'
),
),
],
),
),
),
),
),
),
);
}
}
userProvider
is a
FutureProvider
that returns an
AsyncValue
.
ref.watch(userProvider)
gives you the
AsyncValue
object.
.when()
method handles all three states:
loading
shows a spinner,
error
shows an error message with retry,
and
data
displays the user profile.
ref.refresh(userProvider)
triggers a new fetch
and resets the state to loading.
AsyncValue
offers several advanced methods for common operations:
// 1. map() - Transform the value
final
mappedValue = asyncValue.map(
data: (data) => AsyncValue.data(data.value.toUpperCase()),
loading: (loading) => AsyncValue.loading(),
error: (error) => AsyncValue.error(error.error, error.stackTrace),
);
// 2. mapData() - Transform only the data state
final
mappedData = asyncValue.mapData((user) => user.name);
// 3. maybeMap() - Transform specific states with fallback
final
result = asyncValue.maybeMap(
data: (data) => data.value.name,
orElse: () =>
'No data yet'
,
);
// 4. When with multiple widgets
userAsync.when(
loading: () => Column(
children: [
CircularProgressIndicator(),
Text(
'Loading...'
),
],
),
error: (error, stack) => Column(
children: [
Icon(Icons.error, color: Colors.red),
Text(
'Error: $error'
),
ElevatedButton(
onPressed: () => ref.refresh(userProvider),
child: Text(
'Retry'
),
),
],
),
data: (user) => UserCard(user: user),
);
// 5. Guarding against null values
final
user = asyncValue.valueOrNull;
// Returns null if not in data state
if
(user !=
null
) {
// Safe to use user
}
AsyncValue (Riverpod)
- Type-safe pattern matching
- Built-in loading state
- Built-in error state
- Automatic caching
- Less boilerplate code
- Forces handling all states
Manual State Class
- Manual null checks
- Custom loading boolean
- Custom error field
- Manual caching logic
- More boilerplate
- Can forget to handle states
Forgetting to handle the loading or error state can lead to a poor user experience or crashes. Always handle all three states.
Use
.when()
to ensure all three states are handled explicitly.
asyncValue.value
can be
null
if the state is loading or error.
Always check
hasValue
first.
Use
.when()
to access the data safely, or check
hasValue
before using
.value
.
After an error occurs, the error state persists until the next successful operation. Make sure to handle retries properly.
In the error state, provide a button that calls
ref.refresh()
to retry the operation.
AsyncValue
works the same way with
StreamProvider
. The main difference is
that the
data
state updates multiple times as the stream emits new values.
final
messagesProvider = StreamProvider<Message>((ref) {
return
chatService.messagesStream;
});
class
MessagesWidget
extends
ConsumerWidget {
@override
Widget
build
(BuildContext context, WidgetRef ref) {
final
messagesAsync = ref.watch(messagesProvider);
return
messagesAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error'
),
data: (messages) => ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index].text),
),
),
);
}
}
// ⚠️ Note: With StreamProvider, the data state updates
// every time the stream emits a new value.
When you want to refetch data, use
ref.refresh()
or
ref.invalidate()
.
// ref.refresh() - Immediately invalidates and refetches
ref.refresh(userProvider);
// The state becomes loading again
// ref.invalidate() - Marks as invalid, refetches on next watch
ref.invalidate(userProvider);
// The state stays as-is until the next time it's watched
// Example: Pull-to-refresh
RefreshIndicator(
onRefresh: () async {
ref.refresh(userProvider);
return
Future.value();
},
child: Consumer(
builder: (context, ref, child) {
final
userAsync = ref.watch(userProvider);
return
userAsync.when(
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text(
'Error: $error'
),
data: (user) => Text(user.name),
);
},
),
);
🎯 Key Takeaway
AsyncValue
is the
standard way
to handle asynchronous states in Riverpod.
It provides a type-safe, elegant API for loading, error, and data states.
Always use .when() to handle all three states
, and use
ref.refresh()
to retry failed operations. With AsyncValue, you'll never forget to handle a state again.