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

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 — это обёртки над функциями. Они кэшируют результат указанной функции и позволяют нескольким виджетам получать доступ к одному и тому же значению:

// Это аналог нашей функции fetchUser, но результат кэшируется.
// При многократном использовании будет возвращаться одно и то же значение.
final userProvider = FutureProvider<User>((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:

SynchronousFutureStream
UnmodifiableProviderFutureProviderStreamProvider
ModifiableNotifierProviderAsyncNotifierProviderStreamNotifierProvider

Поначалу это может показаться сложным. Давай разберёмся по шагам.

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 зависит от того, является ли он "изменяемым" или "неизменяемым", как указано в таблице выше.

final name = SomeProvider.someModifier<Result>((ref) {
  <ваша логика здесь>
});
Переменная provider

Эта переменная будет использоваться для взаимодействия с нашим provider. Переменная должна быть final и иметь "верхний уровень" (глобальный).

примечание

Не пугайтесь глобальной природы providers. Providers полностью неизменяемы. Объявление provider ничем не отличается от объявления функции, а сами providers легко тестировать и поддерживать.

Тип provider

Обычно это Provider, FutureProvider или StreamProvider.
Тип используемого provider зависит от возвращаемого значения вашей функции. Например, чтобы создать Future<Activity>, вам понадобится FutureProvider<Activity>.

Чаще всего вам будет нужен именно FutureProvider.

подсказка

Не думайте в духе "Какой provider мне выбрать". Вместо этого подумайте: «Что я хочу вернуть». Тип provider определится сам собой.

Модификаторы (необязательно)

Часто после типа provider можно увидеть "модификатор".
Модификаторы необязательны и используются для безопасного изменения поведения provider с точки зрения типов.

На данный момент доступны два модификатора:

  • autoDispose, автоматически очищает кэш, когда provider перестаёт использоваться.
    См. также Automatic disposal
  • family, позволяет передавать аргументы в provider.
    См. также Family.
Ref

Объект, используемый для взаимодействия с другими providers.
Он есть у всех providers: либо как параметр функции provider, либо как свойство Notifier.

Функция provider

Именно здесь размещается логика providers. Эта функция будет вызвана при первом чтении provider.
Последующие чтения не будут вызывать функцию повторно — вместо этого будет возвращаться закэшированное значение.

к сведению

Вы можете объявлять сколько угодно providers без ограничений. В отличие от ситуации при использовании package:provider, Riverpod позволяет создавать несколько providers, которые предоставляют state одного и того же "типа":

final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((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);
},
);
}
}