Чтение провайдера
Прежде чем перейти к чтения данного гайда, убедитесь, что вы уже изучили Провайдеры.
В этом мануале мы посмотрим, как читать провайдер.
Получение объекта "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}'));