
Handling API calls in Flutter can get messy—especially when you’re dealing with loading states, errors, and updates all at once. That’s where Riverpod Async Notifiers come in.
In this post, you’ll learn how to use riverpod async notifiers the right way to manage state from APIs in a clean, scalable, and reactive way.
🔍 What Are Riverpod Async Notifiers?
Introduced in Riverpod 2.0+, this feature is built specifically for asynchronous operations like API calls. It simplifies managing loading, success, and error states without cluttering your UI or view models.
Unlike StateNotifier
or Notifier
, AsyncNotifier is purpose-built for future-based logic, making it ideal for real-world app development.
🧠 Why Use Riverpod Async Notifiers?
Here’s why this approach is a great fit for managing API-driven state in Flutter:
- Built-in support for
AsyncValue<T>
- Automatically tracks loading, error, and data states
- Keeps your UI reactive and declarative
- Cleaner than manually handling Futures in widgets
- Scales well in larger apps
🛠️ Step-by-Step: Using Riverpod Async Notifiers
Let’s walk through a simple example: fetching user data from an API.
1. Create the Model
class User {
final String name;
final String email;
User({required this.name, required this.email});
}
2. Create the Async Notifier
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
return fetchUser();
}
Future<User> fetchUser() async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
final data = jsonDecode(response.body);
return User(name: data['name'], email: data['email']);
}
}
3. Register the Provider
final userProvider = AsyncNotifierProvider<UserNotifier, User>(() => UserNotifier());
4. Use in Your Widget
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userProvider);
return userState.when(
data: (user) => Text('Hello, ${user.name}'),
loading: () => CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
);
}
}
✅ Best Practices for Riverpod Async Notifiers
The key to using this approach right is isolating your logic, making it reactive, and keeping everything clean and testable. Here are a few tips:
- Always return
AsyncValue<T>
and let the widget layer handle the.when
- Don’t mutate state inside the widget—keep all logic inside the notifier
- Use
ref.read()
orref.watch()
wisely when depending on other providers - Structure your providers per feature, not globally
🔄 Refetching and Manual Updates
You can manually refetch or trigger updates using:
ref.invalidate(userProvider);
Or, inside your notifier:
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => fetchUser());
}
This gives you full control while maintaining the async lifecycle Riverpod manages for you.
🧪 Testing Riverpod Async Notifiers
Testing this pattern is much simpler compared to traditional async logic in Flutter.
void main() {
test('Fetches user data successfully', () async {
final container = ProviderContainer();
final notifier = container.read(userProvider.notifier);
await notifier.fetchUser();
expect(container.read(userProvider).value?.name, isNotEmpty);
});
}
🧠 Conclusion
This approach provides a clean and powerful solution for handling API state in Flutter. It enables you to build predictable, scalable, and testable apps—without boilerplate or complex stream management.