Skip to main content

StateNotifierProvider

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/...)

StateNotifierProvider is a provider that is used to listen to and expose a StateNotifier (from the package state_notifier, which Riverpod re-exports).

It is typically used for:

  • exposing an immutable 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.
info

Prefer using NotifierProvider instead.

As a usage example, we could use StateNotifierProvider 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:


// The state of our StateNotifier should be immutable.
// We could also use packages like Freezed to help with the implementation.

class Todo {
const Todo({required this.id, required this.description, required this.completed});

// All properties should be `final` on our class.
final String id;
final String description;
final bool completed;

// Since Todo is immutable, we implement a method that allows cloning the
// Todo with slightly different content.
Todo copyWith({String? id, String? description, bool? completed}) {
return Todo(
id: id ?? this.id,
description: description ?? this.description,
completed: completed ?? this.completed,
);
}
}

// The StateNotifier class that will be passed to our StateNotifierProvider.
// 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.
class TodosNotifier extends StateNotifier<List<Todo>> {
// We initialize the list of todos to an empty list
TodosNotifier(): super([]);

// 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,
];
}
}

// Finally, we are using StateNotifierProvider to allow the UI to interact with
// our TodosNotifier class.
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

Now that we have defined a StateNotifierProvider, 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),
),
],
);
}
}