FutureProvider → AsyncNotifier
Migrate from FutureProvider to AsyncNotifier when your asynchronous state needs to support mutations, refreshes, or business logic.
FutureProvider is ideal for read-only asynchronous data, while AsyncNotifier is designed for asynchronous state that can change over time.
What is it?
Both FutureProvider and AsyncNotifier manage asynchronous state, but they serve different purposes.
- FutureProvider performs a single asynchronous computation and exposes its result.
- AsyncNotifier manages asynchronous state and allows that state to be modified after it has been loaded.
Think of FutureProvider as read-only, while AsyncNotifier is read and write.
Why does it exist?
FutureProvider is excellent for loading data, but it has an important limitation:
- It cannot expose methods to modify its state.
As applications grow, asynchronous state often needs to:
- Refresh data
- Save changes
- Delete items
- Retry failed requests
- Optimistically update the UI
AsyncNotifier was introduced to support these use cases while keeping asynchronous state inside a single class.
Syntax
FutureProvider
final userProvider =
FutureProvider<User>((ref) async {
return repository.fetchUser();
});
Explanation:
- Loads data once.
- Exposes an
AsyncValue<User>. - Cannot mutate its own state.
AsyncNotifier
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
return repository.fetchUser();
}
}
final userProvider =
AsyncNotifierProvider<UserNotifier, User>(
UserNotifier.new,
);
Explanation:
build()performs the initial asynchronous load.- Additional methods can modify the state later.
Updating state
class UserNotifier extends AsyncNotifier<User> {
@override
Future<User> build() async {
return repository.fetchUser();
}
Future<void> refreshUser() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
return repository.fetchUser();
});
}
}
Explanation:
refreshUser()updates the provider after initialization.- This is not possible with
FutureProvider.
Mental Model
FutureProvider:
Request
│
▼
Response
│
▼
Finished
AsyncNotifier:
Load
│
▼
State
│
├── Refresh
├── Update
├── Delete
└── Retry
AsyncNotifier manages the entire lifecycle of asynchronous state instead of a single request.
Examples
Simple Example
Before
final profileProvider =
FutureProvider<Profile>((ref) async {
return repository.loadProfile();
});
Explanation:
- The profile can only be loaded.
- It cannot be updated directly.
After
class ProfileNotifier
extends AsyncNotifier<Profile> {
@override
Future<Profile> build() async {
return repository.loadProfile();
}
Future<void> reload() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
return repository.loadProfile();
});
}
}
Explanation:
- The notifier can reload the profile whenever needed.
- State management stays in one place.
Real-World Example
class TodoNotifier
extends AsyncNotifier<List<Todo>> {
@override
Future<List<Todo>> build() async {
return repository.fetchTodos();
}
Future<void> addTodo(Todo todo) async {
await repository.add(todo);
state = await AsyncValue.guard(() async {
return repository.fetchTodos();
});
}
}
Explanation:
- Loading and updating todos are handled by the same notifier.
- The UI simply watches the provider.
When to Use
Use AsyncNotifier when you need to:
- Refresh data
- Save changes
- Delete records
- Retry failed requests
- Perform optimistic updates
- Manage asynchronous business logic
- Expose methods to the UI
When NOT to Use
Continue using FutureProvider when:
- Data is read-only.
- Only a single asynchronous request is required.
- No mutations are needed.
- The provider simply loads configuration or reference data.
Use the simplest provider that satisfies your requirements.
Best Practices
- Prefer
FutureProviderfor read-only data. - Use
AsyncNotifierwhen state can change. - Keep asynchronous business logic inside the notifier.
- Use
AsyncValue.guard()for error handling. - Expose meaningful methods instead of manipulating
statefrom the UI.
Common Mistakes
Using FutureProvider for mutable state
Wrong:
final todosProvider =
FutureProvider<List<Todo>>(...);
Explanation:
- There is no place to add, edit, or remove todos.
Correct:
final todosProvider =
AsyncNotifierProvider<
TodoNotifier,
List<Todo>>(
TodoNotifier.new,
);
Explanation:
- The notifier owns both loading and mutations.
Refreshing manually everywhere
Wrong:
ref.refresh(userProvider);
Explanation:
- The UI becomes responsible for state management.
Correct:
ref
.read(userProvider.notifier)
.refreshUser();
Explanation:
- Business logic stays inside the notifier.
Putting repository logic in the UI
Wrong:
await repository.saveUser(user);
Explanation:
- The widget directly accesses the data layer.
Correct:
await ref
.read(userProvider.notifier)
.saveUser(user);
Explanation:
- The notifier coordinates repository operations.
- The UI remains focused on presentation.
Related APIs
- FutureProvider
- AsyncNotifier
- AsyncNotifierProvider
- AsyncValue
AsyncValue.guard()ref.watch()ref.read()
Summary
FutureProvider is best for read-only asynchronous data, while AsyncNotifier is the preferred choice for asynchronous state that needs to change over time. When your provider needs methods such as refresh, save, delete, or retry, migrating to AsyncNotifier results in cleaner, more maintainable business logic and a better separation of concerns.