Skip to main content

(Async)NotifierProvider

NotifierProvider is a provider that is used to listen to and expose a Notifier.
AsyncNotifierProvider is a provider that is used to listen to and expose an AsyncNotifier. AsyncNotifier is a Notifier that can be asynchronously initialized.
(Async)NotifierProvider along with (Async)Notifier is Riverpod's recommended solution for managing state which may change in reaction to a user interaction.

It is typically used for:

  • exposing a state which can change over time after reacting to custom events.
  • centralizing the logic for modifying some state (aka "business logic") in a single place, improving maintainability over time.

As a usage example, we could use NotifierProvider to implement a todo-list. Doing so would allow us to expose methods such as addTodo to let the UI modify the list of todos on user interactions:



class Todo with _$Todo {
factory Todo({
required String id,
required String description,
required bool completed,
}) = _Todo;
}

// This will generates a Notifier and NotifierProvider.
// The Notifier class that will be passed to our NotifierProvider.
// This class should not expose state outside of its "state" property, which means
// no public getters/properties!
// The public methods on this class will be what allow the UI to modify the state.
// Finally, we are using todosProvider(NotifierProvider) to allow the UI to
// interact with our Todos class.

class Todos extends _$Todos {

List<Todo> build() {
return [];
}

// Let's allow the UI to add todos.
void addTodo(Todo todo) {
// Since our state is immutable, we are not allowed to do `state.add(todo)`.
// Instead, we should create a new list of todos which contains the previous
// items and the new one.
// Using Dart's spread operator here is helpful!
state = [...state, todo];
// No need to call "notifyListeners" or anything similar. Calling "state ="
// will automatically rebuild the UI when necessary.
}

// Let's allow removing todos
void removeTodo(String todoId) {
// Again, our state is immutable. So we're making a new list instead of
// changing the existing list.
state = [
for (final todo in state)
if (todo.id != todoId) todo,
];
}

// Let's mark a todo as completed
void toggle(String todoId) {
state = [
for (final todo in state)
// we're marking only the matching todo as completed
if (todo.id == todoId)
// Once more, since our state is immutable, we need to make a copy
// of the todo. We're using our `copyWith` method implemented before
// to help with that.
todo.copyWith(completed: !todo.completed)
else
// other todos are not modified
todo,
];
}
}

Now that we have defined a NotifierProvider, we can use it to interact with the list of todos in our UI:


class TodoListView extends ConsumerWidget {
const TodoListView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// rebuild the widget when the todo list changes
List<Todo> todos = ref.watch(todosProvider);

// Let's render the todos in a scrollable list view
return ListView(
children: [
for (final todo in todos)
CheckboxListTile(
value: todo.completed,
// When tapping on the todo, change its completed status
onChanged: (value) =>
ref.read(todosProvider.notifier).toggle(todo.id),
title: Text(todo.description),
),
],
);
}
}

As a usage example, we could use AsyncNotifierProvider to implement a remote todo-list. Doing so would allow us to expose methods such as addTodo to let the UI modify the list of todos on user interactions:



class Todo with _$Todo {
factory Todo({
required String id,
required String description,
required bool completed,
}) = _Todo;

factory Todo.fromJson(Map<String, dynamic> json) => _$TodoFromJson(json);
}

// This will generates a AsyncNotifier and AsyncNotifierProvider.
// The AsyncNotifier class that will be passed to our AsyncNotifierProvider.
// This class should not expose state outside of its "state" property, which means
// no public getters/properties!
// The public methods on this class will be what allow the UI to modify the state.
// Finally, we are using asyncTodosProvider(AsyncNotifierProvider) to allow the UI to
// interact with our Todos class.

class AsyncTodos extends _$AsyncTodos {
Future<List<Todo>> _fetchTodo() async {
final json = await http.get('api/todos');
final todos = jsonDecode(json) as List<Map<String, dynamic>>;
return todos.map(Todo.fromJson).toList();
}


FutureOr<List<Todo>> build() async {
// Load initial todo list from the remote repository
return _fetchTodo();
}

Future<void> addTodo(Todo todo) async {
// Set the state to loading
state = const AsyncValue.loading();
// Add the new todo and reload the todo list from the remote repository
state = await AsyncValue.guard(() async {
await http.post('api/todos', todo.toJson());
return _fetchTodo();
});
}

// Let's allow removing todos
Future<void> removeTodo(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.delete('api/todos/$todoId');
return _fetchTodo();
});
}

// Let's mark a todo as completed
Future<void> toggle(String todoId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await http.patch(
'api/todos/$todoId',
<String, dynamic>{'completed': true},
);
return _fetchTodo();
});
}
}

Now that we have defined a AsyncNotifierProvider, we can use it to interact with the list of todos in our UI:


class TodoListView extends ConsumerWidget {
const TodoListView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// rebuild the widget when the todo list changes
final asyncTodos = ref.watch(asyncTodosProvider);

// Let's render the todos in a scrollable list view
return switch (asyncTodos) {
AsyncData(:final value) => ListView(
children: [
for (final todo in value)
CheckboxListTile(
value: todo.completed,
// When tapping on the todo, change its completed status
onChanged: (value) {
ref.read(asyncTodosProvider.notifier).toggle(todo.id);
},
title: Text(todo.description),
),
],
),
AsyncError(:final error) => Text('Error: $error'),
_ => const Center(child: CircularProgressIndicator()),
};
}
}