К содержимому

Provider

Provider самый простой из всех провайдеров. Он создает значение... И на этом все.

Provider обычно используется для:

  • кэширования вычислений
  • предоставления значение другим провайдерам (например Repository/HttpClient)
  • предоставления возможности изменения своего значения
  • уменьшения количество перестроек провайдеров/виджетов без надобности использовать select

Использование Provider для кэширования вычислений

Provider в комбинации с ref.watch является мощным инструментом для кэширования синхронных операций

Примером может быть фильтрация списка задач. Т. к. фильтрация списка может быть достаточно затратной, не стоит фильтровать список каждый раз, когда приложение перерисовывается. В данном случае мы можем воспользоваться Provider.

Предположим, что у нас уже есть StateNotifierProvider, который управляет списком задач:


class Todo {
Todo(this.description, this.isCompleted);
final bool isCompleted;
final String description;
}

class TodosNotifier extends StateNotifier<List<Todo>> {
TodosNotifier() : super([]);

void addTodo(Todo todo) {
state = [...state, todo];
}
// TODO: добавить остальные методы (removeTodo, ...)
}

final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
return TodosNotifier();
});

Тут мы можем воспользоваться Provider для хранения выполненных задач:


final completedTodosProvider = Provider<List<Todo>>((ref) {
// Получаем список всех задач из todosProvider
final todos = ref.watch(todosProvider);

// Возвращаем только выполненные задачи
return todos.where((todo) => todo.isCompleted).toList();
});

Теперь наш UI может отобразить список выполненных задач, прослушивая completedTodosProvider:

Consumer(builder: (context, ref, child) {
final completedTodos = ref.watch(completedTodosProvider);
// TODO: отобразить задачи с помощью ListView/GridView/.../* SKIP */
return Container();
/* SKIP END */
});

Самое интересное заключается в том, что теперь отфильтрованный список закэширован.

Это означает, что список выполненных задач не будет перевычислен, пока не будет выполнено увеличение / уменьшение / обновление списка задач. Даже многократное чтение списка завершенных задач не повлечет за собой перевычисление.

Обратите внимание на то, что нам не нужно вручную обновлять закэшированное значение. Благодаря ref.watch Provider сам знает, когда ему нужно осуществить перевычисление.

Уменьшение количества перестроек провайдера/виджета с помощью Provider

Особенность Provider заключается в том, что он не перестраивает виджеты/провайдеры пока не закончит перевычисление.

Реальным примером может послужить активация/деактивация кнопок "следующая/предыдущая страница" для реализации пагинации:

пример пагинации

В нашем случае сфокусируемся на кнопке "предыдущая страница". Самый простой вариант реализации - виджет, который получает текущий индекс страницы, и, если индекс равен 0, кнопка становится неактивна.

Код будет выглядеть так:


final pageIndexProvider = StateProvider<int>((ref) => 0);

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


Widget build(BuildContext context, WidgetRef ref) {
// Если мы не на первой странице, тогда кнопка перехода
// на предыдущую страницу активна
final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

Проблема в том, что наша кнопка будет перестраиваться каждый раз при изменении страницы. В идеале кнопка должна перестраиваться, только когда нужно сделать ее активной/неактивной.

Корень данной проблемы в том, что мы определяем состояние кнопки внутри виджета.

Лучшим решением будет перемещение логики кнопки в Provider:


final pageIndexProvider = StateProvider<int>((ref) => 0);

// Провайдер, который определяет, может ли пользователь вернуться на
// предыдущую страницу
final canGoToPreviousPageProvider = Provider<bool>((ref) {
return ref.watch(pageIndexProvider) != 0;
});

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


Widget build(BuildContext context, WidgetRef ref) {
// Наблюдаем за нашим провайдером
// Наш виджет больше не определяет, можно ли вернуться на
// предыдущую страницу или нет
final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

void goToPreviousPage() {
ref.read(pageIndexProvider.notifier).update((state) => state - 1);
}

return ElevatedButton(
onPressed: canGoToPreviousPage ? goToPreviousPage : null,
child: const Text('previous'),
);
}
}

После небольшого рефакторинга PreviousButton больше не перестраивается каждый раз при изменение индекса страницы благодаря Provider.

Теперь каждый раз при изменении индекса значение canGoToPreviousPageProvider перевычисляется. А PreviousButton перестраивается, только когда значение canGoToPreviousPageProvider изменяется.

Данное изменение повысило производительность нашей кнопки и позволило вынести логику из виджета.