Providers
Providers — ключевая возможность Riverpod. Если вы используете Riverpod, то используете его из-за providers.
Что такое provider?
Providers по сути являются «мемоизированными функциями» с дополнительными возможностями.
Это означает, что providers — это функции, которые возвращают кэшированное значение при вызове с одинаковыми параметрами.
Наиболее распространенный сценарий использования провайдеров — это выполнение сетевого запроса.
Рассмотрим функцию, которая получает данные пользователя из API:
Future<User> fetchUser() async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
Одна из проблем этой функции заключается в том, что если бы мы попытались использовать её внутри виджетов, нам пришлось бы кэшировать результат самостоятельно, а затем найти способ поделиться этим значением со всеми виджетами, которым оно необходимо.
Именно здесь providers приходят на помощь. Providers — это обёртки над функциями. Они кэшируют результат указанной функции и позволяют нескольким виджетам получать доступ к одному и тому же значению:
- riverpod
- riverpod_generator
// Это аналог нашей функции fetchUser, но результат кэшируется.
// При многократном использовании будет возвращаться одно и то же значение.
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
});
// Это аналог нашей функции fetchUser, но результат кэшируется.
// В результате будет создан "userProvider". При многократном использовании
// будет возвращаться одно и то же значение.
Future<User> user(Ref ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
Помимо базового кэширования, providers также предоставляют различные возможности, которые делают их более мощными:
- Встроенные механизмы инвалидации кэша
В частности, Ref.watch позволяет объединять кэши между собой, автоматически инвалидируя всё необходимое. - Automatic disposal
Providers могут автоматически освобождать ресурсы, когда они больше не нужны. - Связывание данных (Data-binding)
Providers устраняют необходимость использовать FutureBuilder или StreamBuilder. - Автоматическая обработка ошибок
Providers могут автоматически перехватывать ошибки и передавать их в UI. - Поддержка моков
Для удобства тестирования и других целей все providers могут быть замоканы. См. Provider overrides. - Offline persistence (experimental)
Результат provider может сохраняться на диск и автоматически загружаться при перезапуске приложения. - Mutations (experimental)
Providers предоставляют встроенный способ отображать в UI состояние загрузки/ошибки для побочных эффектов, таких как отправка формы.
Существует 6 типов providers:
| Synchronous | Future | Stream | |
|---|---|---|---|
| Unmodifiable | Provider | FutureProvider | StreamProvider |
| Modifiable | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
Поначалу это может показаться сложным. Давай разберёмся по шагам.
Sync vs Future vs Stream:
Столбцы этой таблицы представляют собой встроенные типы данных Dart для функций.
int synchronous() => 0;
Future<int> future() async => 0;
Stream<int> stream() => Stream.value(0);
Неизменяемые vs Изменяемые:
По умолчанию providers нельзя изменять из виджетов.
Вариант providers на основе Notifier позволяет изменять их извне.
Это похоже на ситуацию с приватным сеттером ("неизменяемые" providers).
// _state можно изменять изнутри,
// но нельзя изменять извне
var _state = 0;
int get state => _state;
В отличие от публичного сеттера ("изменяемые" providers)
// Изменить "state" можно откуда угодно
var state = 0;
Также неизменяемые и изменяемые можно в принципе рассматривать как StatelessWidget и StatefulWidget соответственно.
Это не совсем точно, так как providers — не виджеты, и оба типа хранят "state". Но принцип похож: "Один объект, неизменяемый" и "Два объекта, изменяемый".
Создание provider
Providers следует создавать как «верхнеуровневые» объявления.
Это означает, что их нужно объявлять вне любых классов или функций.
Синтаксис создания provider зависит от того, является ли он "изменяемым" или "неизменяемым", как указано в таблице выше.
- Неизменяемый (functional)
- Изменяемый (notifier)
- riverpod
- riverpod_generator
final name = SomeProvider.someModifier<Result>((ref) { <ваша логика здесь> });
| Переменная provider | Эта переменная будет использоваться для взаимодействия с нашим provider. Переменная должна быть final и иметь "верхний уровень" (глобальный). примечание Не пугайтесь глобальной природы providers. Providers полностью неизменяемы. Объявление provider ничем не отличается от объявления функции, а сами providers легко тестировать и поддерживать. |
| Тип provider | Обычно это Provider, FutureProvider или StreamProvider. Чаще всего вам будет нужен именно подсказка Не думайте в духе "Какой provider мне выбрать". Вместо этого подумайте: «Что я хочу вернуть». Тип provider определится сам собой. |
| Модификаторы (необязательно) | Часто после типа provider можно увидеть "модификатор". На данный момент доступны два модификатора:
|
| Ref | Объект, используемый для взаимодействия с другими providers. |
| Функция provider | Именно здесь размещается логика providers. Эта функция будет вызвана при первом чтении provider. |
@riverpod Result myFunction(Ref ref) { <ваша логика здесь> }
| Аннотация | Все сгенерированные providers должны быть помечены Например, мы можем отключить "auto-dispose" (о котором поговорим позже) указав |
| Аннотированная функция | Название аннотированной функции определяет, как будет осуществляться взаимодействие с provider. Аннотированные функции должны принимать Ref в качестве первого параметра. Эта функция будет вызвана при первом чтении provider. |
| Ref | Объект, используемый для взаимодействия с другими providers. |
- riverpod
- riverpod_generator
final name = SomeNotifierProvider.someModifier<MyNotifier, Result>(MyNotifier.new); class MyNotifier extends SomeNotifier<Result> { @override Result build() { <ваша логика здесь> } <your methods here> }
| Переменная provider | Эта переменная будет использоваться для взаимодействия с нашим provider. Переменная должна быть final и иметь "верхний уровень" (глобальный). примечание Не пугайтесь глобальной природы providers. Providers полностью неизменяемы. Объявление provider ничем не отличается от объявления функции, а сами providers легко тестировать и поддерживать. |
| Тип provider | Обычно это NotifierProvider, AsyncNotifierProvider или StreamNotifierProvider. Чаще всего вам будет нужен именно AsyncNotifierProvider. подсказка Не думайте в духе "Какой provider мне выбрать". Создавайте любой state, который вам нужен, а тип provider определится сам собой. |
| Модификаторы (необязательно) | Часто после типа provider можно увидеть "модификатор". На данный момент доступны два модификатора:
|
| Конструктор Notifier | Параметром "notifier providers" является функция, которая должна создавать экземпляр "notifier". |
| Notifier | Если Этот класс отвечает за предоставление способов изменения состояния provider. предупреждение Не размещайте логику в конструкторе вашего notifier. |
| Тип Notifier | Базовый класс, который расширяет ваш notifier должен соответствовать типу provider и "family" если он используется. Вот несколько примеров:
|
| Метод build | Все notifiers должны переопределять метод Этот метод не следует вызывать напрямую. |
@riverpod class MyNotifier extends _$MyNotifier { @override Result build() { <ваша логика здесь> } <your methods here> }
| Аннотация | Все сгенерированные providers должны быть помечены Например, мы можем отключить "auto-dispose" (о котором поговорим позже) указав |
| Notifier | Когда аннотация Notifiers отвечают за предоставление способов изменения состояния provider. предупреждение Не размещайте логику в конструкторе вашего notifier. |
| Метод build | Все notifiers должны переопределять метод Этот метод не следует вызывать напрямую. |
Вы можете объявлять сколько угодно providers без ограничений.
В отличие от ситуации при использовании package:provider, Riverpod позволяет создавать несколько providers, которые предоставляют state одного и того же "типа":
- riverpod
- riverpod_generator
final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((ref) => 'England');
String city(Ref ref) => 'London';
String country(Ref ref) => 'England';
Тот факт, что оба поставщика создают String, не вызывает никаких проблем.
Использование providers
Providers похожи на widgets тем, что сами по себе они ничего не делают.
Как widgets являются описанием UI, так providers являются описанием состояния.
Удивительно, но provider полностью stateless и мог бы создаваться через const,
если бы это не усложняло синтаксис.
Чтобы использовать provider, нужен отдельный объект: ProviderContainer. Подробнее см. ProviderContainers/ProviderScopes.
Короче говоря, прежде чем использовать provider, оберните ваши Flutter-приложения в ProviderScope:
void main() {
runApp(ProviderScope(child: MyApp()));
}
После этого вам понадобится получить Ref, чтобы взаимодействовать с вашими providers. См. Refs для подробностей.
Вкратце, есть два способа получить Ref:
- Providers получают доступ к нему автоматически.
Это первый параметр функции provider или свойствоrefу Notifier. Это позволяет providers взаимодействовать друг с другом. - Дереву widgets нужны специальные виджеты, называемые Consumers. Эти виджеты служат связующим звеном между widget tree и provider tree, предоставляя WidgetRef.
В качестве примера рассмотрим helloWorldProvider который возвращает простую строку.
Использовать его в widgets можно так:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, _) {
// Получение значения provider
final helloWorld = ref.watch(helloWorldProvider);
// Использование значения в UI
return Text(helloWorld);
},
);
}
}