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

Чтение провайдера

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

В этом мануале мы посмотрим, как читать провайдер.

Получение объекта "ref"

Перед чтением провайдера, нам необходимо получить "ref".

Этот объект позволяет нам взаимодействовать с провайдерами из виджета или другого провайдера.

Получение объекта "ref" из провайдера

Все провайдеры получают "ref" в качестве параметра:

final provider = Provider((ref) {
// используем ref для взаимодействия с другим провайдером
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

Этот параметр можно безопасно передавать в хранимое значение.

Например, распространенным случаем является передача "ref" в StateNotifier:


final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref): super(0);

final Ref ref;

void increment() {
// Counter может использовать "ref" для чтения других провайдеров
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

Это предоставляет классу Counter возможность читать провайдеры.

Получение объекта "ref" из виджета

Изначально виджеты не имеют параметра ref. Riverpod предлагает несколько решений данной проблемы.

Наследование от ConsumerWidget вместо StatelessWidget

Самым распространенным способом получения ref внутри виджета является замена StatelessWidget на ConsumerWidget.

ConsumerWidget идентичен StatelessWidget в использовании с единственной разницей в том, что метод build имеет дополнительный параметр: "ref".

Обычный ConsumerWidget выглядит подобным образом:


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


Widget build(BuildContext context, WidgetRef ref) {
// использование ref для прослушивания провайдера
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Наследование от ConsumerStatefulWidget+ConsumerState вместо StatefulWidget+State

Подобно виджету ConsumerWidget, существуют ConsumerStatefulWidget и ConsumerState, являющиеся эквивалентом StatefulWidget и State, с разницей в том, что ConsumerState имеет "ref".

В этом случае "ref" не передается в метод build как параметр, а является полем объекта ConsumerState.


class HomeView extends ConsumerStatefulWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" можно использовать внутри каждого метода жизненного цикла StatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// Также мы можем использовать ref внутри метода build
// для прослушивания провайдеров.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Наследование от HookConsumerWidget вместо HookWidget

Данное решение предназначено для пользователей flutter_hooks. Т. к. данный пакет требует наследования от HookWidget, то виджеты, использующие хуки, не могу наследоваться от ConsumerWidget.

Пакет hooks_riverpod предоставляет виджет HookConsumerWidget. Данный виджет ведет себя как ConsumerWidget и HookWidget. Такое решение позволяет слушать провайдеры и использовать хуки.

Например:


class HomeView extends HookConsumerWidget {
const HomeView({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// HookConsumerWidget позволяет использовать хуки внутри метода build
final state = useState(0);

// Также мы можем использовать ref для прослушивания провайдеров.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Наследование от StatefulHookConsumerWidget вместо HookWidget

Данный вариант подходит тем, кому необходимы методы жизненного цикла StatefulWidget в добавок к хукам.

Например:


class HomeView extends StatefulHookConsumerWidget {
const HomeView({super.key});


HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {

void initState() {
super.initState();
// "ref" можно использовать внутри каждого метода жизненного цикла StatefulWidget.
ref.read(counterProvider);
}


Widget build(BuildContext context) {
// Мы можем использовать хуки внутри builder, как и в HookConsumerWidget
final state = useState(0);

// Также мы можем использовать ref внутри метода build
// для прослушивания провайдеров.
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Consumer и HookConsumer

И последним способом получения "ref" внутри виджета является использование Consumer/HookConsumer.

Эти виджеты имеют те же свойства, что и ConsumerWidget/HookConsumerWidget, и могут быть использованы для получения "ref" в функции builder.

По факту, эти виджеты можно использовать для получения "ref" без нужды создавать класс. Например:

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// Мы можем использовать хуки внутри builder, как и в HookConsumerWidget
final state = useState(0);

// Также мы можем использовать ref для прослушивания провайдеров.
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);

Использование ref для взаимодействия с провайдерами

Теперь, когда мы имеем "ref", мы можем использовать его.

Существует три основных варианта использования "ref":

  • получение значения провайдера и подписка на изменения. В таком случае, при смене состояния наблюдаемого провайдера произойдет перестройка наблюдающего виджета или провайдера. Для этого используем ref.watch.
  • подписка на изменения провайдера для выполнения какого-либо действия. Например навигация или открытие модального окна при изменении состояния провайдера. Для этого используем ref.listen.
  • одноразовое чтение значения провайдера без подписки на изменения. Например получение значения провайдера по нажатию кнопки. Для этого используем ref.read.
примечание

Рекомендуется использовать ref.watch или же ref.listen вместо ref.read. Используя ref.watch, вы делаете приложение реактивным и декларативным, что упрощает его поддержку.

Использование ref.watch для наблюдения за провайдером

ref.watch используется внутри метода build виджета или в теле провайдера для прослушивания изменений другого провайдера.

Например провайдер может использовать ref.watch для комбинации нескольких провайдеров в одном новом значении.

В качестве примера рассмотрим фильтрацию списка задач. Допустим, у нас есть два провайдера:

  • filterTypeProvider - провайдер, хранящий текущий тип фильтра (без фильтра, только выполненные задачи, ...)
  • todosProvider - провайдер, хранящий полный список всех задач

Теперь с помощью ref.watch мы можем создать третий провайдер, который на основе значений двух других провайдеров будет создавать отфильтрованный список задач:


final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
// получение фильтра и полного списка задач
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// возвращает список выполненных задач
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// возвращает полный список всех задач
return todos;
}
});

Теперь filteredTodoListProvider хранит отфильтрованный список задач.

Отфильтрованный список будет автоматически обновляться при изменении хотя бы одного из провайдеров: filterTypeProvider, todosProvider. Если же ни один из провайдеров не изменил свое значение, то filteredTodoListProvider также не изменит своего значения.

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


final counterProvider = StateProvider((ref) => 0);

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


Widget build(BuildContext context, WidgetRef ref) {
// использование ref для прослушивания провайдера
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

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

предупреждение

Метод watch не следует вызывать асинхронно, например в onPressed ElevatedButton. Также watch не стоит использовать внутри initState и других методов жизненного цикла State.

В этих случаях используйте ref.read.

Использование ref.listen для реагирования на изменения провайдера

Аналогично ref.watch можно использовать ref.listen для наблюдения за провайдером.

Главная разница между ref.watch и ref.listen заключается в том, что ref.listen не перестраивает виджет/провайдер, а вызывает определенную функцию.

Это можно использовать для отображения SnackBar, когда случается ошибка.

ref.listen принимает 2 позиционных аргумента: первый - провайдер для наблюдения, второй - функция, которая должна вызываться при изменении значения провайдера. Причем функция должна принимать 2 параметра: предыдущее и новое состояния провайдера.

ref.listen можно использовать внутри тела провайдера:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});

или внутри метода build виджета:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

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


Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
предупреждение

Метод listen не следует вызывать асинхронно, например в onPressed ElevatedButton. Также listen не стоит использовать внутри initState и других методов жизненного цикла State.

Использование ref.read для получения значения провайдера

Метод ref.read позволяет единожды прочесть значение провайдера без подписки на его изменения.

Данный метод часто используется внутри функций, которые вызываются при определенном действии пользователя. Например, мы можем использовать ref.read для увеличения счетчика, когда пользователь нажимает кнопку:


final counterProvider = StateNotifierProvider<Counter, int>(Counter.new);

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


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// Вызов `increment()` класса `Counter`
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
примечание

Следует максимально избегать использования ref.read , т. к. этот метод не реактивен.

Его следует использовать только тогда, когда использование watch и listen может вызвать проблему. При возможности используйте watch/listen.

НЕ используйте ref.read внутри метода build

Возможно, вы захотите оптимизировать производительность виджета с помощью ref.read:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
// использование "read" для единоразового чтения значения провайдера
// без подписки на его изменения
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

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

Вы можете подумать: "Значение провайдера никогда не изменится, так что лучше использовать 'ref.read'". Однако нет никаких гарантий, что завтра этот провайдер будет вести себя так же, как и сегодня.

ПО имеет тенденцию меняться, так что в будущем может понадобиться изменить значение, которое никогда не изменялось. Если вы использовали ref.read, вам придется пройтись по всему проекту и заменить ref.read на ref.watch.

С ref.watch у вас будет меньше проблем при рефакторинге.

Но я хочу использовать ref.read для уменьшения количества перестроек виджета

Хотя ваши намерения похвальны, стоит отметить, что вы можете добиться того же эффекта (уменьшения количества перестроек), используя ref.watch.

Провайдеры предоставляют несколько способов получения значения с учетом уменьшения количества перестроек.

Например, вместо этого:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

мы можем сделать это:


final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

Оба примера работают одинаково: кнопка не будет перестраиваться при изменении счетчика.

С другой стороны, второй вариант учитывает случай, когда счетчик сбрасывается. Например где-то в другом месте приложения мы можем вызвать:

ref.refresh(counterProvider);

что пересоздаст StateController.

Если мы будем использовать ref.read, то наша кнопка до сих пор будет использовать предыдущий StateController, который был аннулирован и больше не должен использоваться. В то время как ref.watch перестоил кнопку и предоставил ей новый StateController.

Определяем что читать

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

В качестве примера рассмотрим StreamProvider:

final userProvider = StreamProvider<User>(...);

При чтении userProvider вы можете:

  • синхронно получать текущее значение, слушая userProvider:

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • получать соответствующий Stream, слушая userProvider.stream:

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • получать Future, который соответствует последнему значению, слушая userProvider.future:

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

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

Использование "select" для контроля перестроек

Напоследок следует упомянуть возможность уменьшения количества перестроек виджета/провайдера при использовании ref.watch или же количества вызовов сторонней функции при использовании ref.listen.

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

Например провайдер может хранить объект User:

abstract class User {
String get name;
int get age;
}

Но виджету необходимо только поле name:

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

Если мы просто воспользуемся ref.watch, то виджет будет перестраиваться при изменении не только name, но и age.

Решить данную задачу можно путем использования select. Так мы сообщаем Riverpod, что хотим слушать только поле name класса User.

Обновленная версия кода выглядит так:

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}

С помощью select мы можем указать функцию, которая будет возвращать волнующее нас поле.

Каждый раз при изменении User Riverpod вызывает нашу функцию и сравнивает предыдущий результат ее выполнения с новым. Если они различаются (т.е. name изменилось), Riverpod перестраивает виджет. Но, если результаты функции одинаковы (т.е. изменился только age), Riverpod не перестраивает наш виджет.

к сведению

Также можно использовать select вместе с ref.listen:

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

Таким образом, ref.listen будет вызывать сторонную функцию, только когда name изменилось.

подсказка

Вы не обязаны возвращать именно поле объекта. Любое значение, которое переопределяет оператор ==, подойдет. Например можно сделать так:

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));