Skip to main content

(Async)NotifierProvider

caution

The content of this page may be outdated.
It will be updated in the future, but for now you may want to refer to the content in the top of the sidebar instead (introduction/essentials/case-studies/...)

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()),
};
}
}