StateNotifierProvider
StateNotifierProvider - провайдер, который можно слушать, а также он хранит в
себе StateNotifier (из пакета state_notifier, который Riverpod экспортирует).
StateNotifierProvider с StateNotifier рекомендуется Riverpod для
управления состоянием, которое может изменяться в ответ на действие пользователя.
Обычно он используется для:
- хранения неизменяемого состояния, которое может быть изменено в ответ на определенное событие
- расположения логики модификации состояния (бизнес логики) в одном месте, тем самым упрощая поддержку кода
В качестве примера, воспользуемся StateNotifierProvider для реализации списка задач.
// Состояние StateNotifier должно быть неизменяемым.
// Для реализации этого мы можем воспользоваться пакетами как Freezed
class Todo {
  const Todo({required this.id, required this.description, required this.completed});
  // В классе все поля должны быть `final`.
  final String id;
  final String description;
  final bool completed;
  // Т. к. Todo неизменяемый, создадим метод клонирования
  // с возможными изменениями
  Todo copyWith({String? id, String? description, bool? completed}) {
    return Todo(
      id: id ?? this.id,
      description: description ?? this.description,
      completed: completed ?? this.completed,
    );
  }
}
// StateNotifier, который будет передан в наш StateNotifierProvider.
// Этот класс не должен предоставлять никаких способов получения своего состояния,
// хранящегося в поле "state",
// т.е. никаких публичных геттеров/полей не должно быть!
// Публичные методы описывают то, как UI может изменять состояние.
class TodosNotifier extends StateNotifier<List<Todo>> {
  // Инициализируем список задач пустым списком
  TodosNotifier(): super([]);
  // Добавление задач
  void addTodo(Todo todo) {
    // Т. к. наше состояние неизменяемо, нельзя делать подобное: `state.add(todo)`.
    // Вместо этого мы должны создать новый список задач, который будет
    // содержать предыдущие задачи и новую.
    // Тут нам поможет spread оператор
    state = [...state, todo];
    // Не нужно вызывать "notifyListeners" или что-то подобное.
    // Вызов "state =" автоматически перестраивает UI, когда это необходимо.
  }
  // Удаление задач
  void removeTodo(String todoId) {
    // Не забываем, что наше состояние неизменяемо.
    // Так что мы создаем новый список, а не изменяем существующий.
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }
  // Изменение статуса задачи: выполнена/не выполнена
  void toggle(String todoId) {
    state = [
      for (final todo in state)
        // Изменяем статус только той задачи,
        // у которой id равен todoId
        if (todo.id == todoId)
          // Еще раз вспомним, что наше состояние неизменяемо. Так что
          // нам необходимо создавать копию списка задач.
          // Воспользуемся методом `copyWith`, который мы реализовали ранее
          todo.copyWith(completed: !todo.completed)
        else
          // Другие задачи не изменяем
          todo,
    ];
  }
}
// Используем StateNotifierProvider для взаимодействия с TodosNotifier
final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
  return TodosNotifier();
});
Теперь, когда мы объявили StateNotifierProvider, мы можем использовать его
для взаимодействия со списком задач:
class TodoListView extends ConsumerWidget {
  const TodoListView({super.key});
  
  Widget build(BuildContext context, WidgetRef ref) {
    // перестройка виджета, когда список задач изменился
    List<Todo> todos = ref.watch(todosProvider);
    // Отображение задач в прокручиваемом списке
    return ListView(
      children: [
        for (final todo in todos)
          CheckboxListTile(
            value: todo.completed,
            // По клику меняем статус задачи
            // выполнена/не выполнена
            onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
            title: Text(todo.description),
          ),
      ],
    );
  }
}