ProviderContainers/ProviderScopes
ProviderContainer — центральный элемент архитектуры Riverpod.
В Riverpod Providers сами по себе не хранят состояние.
Вместо этого состояние конкретного provider хранится внутри этого объекта-контейнера.
ProviderScope — это виджет, который создаёт ProviderContainer и предоставляет его дереву виджетов.
Именно поэтому при использовании Riverpod вы всегда увидите scope в корне приложения.
Без него Riverpod не смог бы хранить состояние providers!
Использование ProviderContainer в приложениях на чистом Dart
ProviderContainer — полезный объект, если вы хотите использовать Riverpod в проектах на чистом Dart, например в консольных или серверных приложениях.
Вы можете создать ProviderContainer в функции main и использовать его для чтения и изменения providers:
import 'package:riverpod/riverpod.dart';
void main() {
final container = ProviderContainer();
try {
final sub = container.listen(counterProvider, (previous, next) {
print('Counter changed from $previous to $next');
});
print('Counter starts at ${sub.read()}');
} finally {
// Освободите контейнер после завершения работы
container.dispose();
}
}
В тестах не используйте ProviderContainer напрямую. Вместо этого используйте ProviderContainer.test. Это автоматически освободит контейнер после завершения теста.
test('Counter starts at 0 and can be incremented', () {
// Нет необходимости освобождать контейнер после завершения теста
final container = ProviderContainer.test();
// Используйте контейнер для тестирования своих providers
});
Получение списка активных providers в контейнере
ProviderContainer предоставляет метод allProviders(), который возвращает активные providers,
находящиеся в данном контейнере и его родительских контейнерах.
Это может быть полезно для отладки, тестирования или применения одной и той же операции ко всем активным providers:
final foo = Provider((ref) => 0);
final bar = Provider((ref) => 1);
// Считываем providers, чтобы сделать их активными
container.read(foo);
container.read(bar);
// Выведет (foo, bar)
print(container.allProviders());
При необходимости можно указать family, чтобы получить только активные providers конкретного family:
final foo = Provider.family<int, int>((ref, id) => id);
final bar = Provider((ref) => 0);
// Считываем providers, чтобы сделать их активными
container.read(foo(0));
container.read(foo(1));
container.read(bar);
// Выведет (foo(0), foo(1))
print(container.allProviders(family: foo));
Использование ProviderScope в Flutter-приложениях
В Flutter-приложениях не следует использовать ProviderContainer напрямую. Вместо этого используйте ProviderScope, который является виджетом-аналогом ProviderContainer.
Конечный результат будет тем же: создайте ProviderScope в функции main. После этого вы сможете использовать
Consumers для чтения и изменения providers в своих виджетах.
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
void main() {
runApp(
ProviderScope(
child: Consumer(
builder: (context, ref, _) {
final counter = ref.watch(counterProvider);
// TODO используйте "counter"
},
),
),
);
}
Зачем хранить состояние providers внутри контейнера?
Можно задаться вопросом: почему providers не хранят своё состояние самостоятельно. Если это требование убрать, можно было бы представить мир, в котором мы писали бы:
print(helloWorldProvider.value); // Выведет "Hello world!"
вместо того, чтобы писать ref.watch(helloWorldProvider).
Riverpod делает это по нескольким причинам, которые сводятся к одной идее: "Никакого глобального состояния".
Лучшее разделение ответственности. Если бы Riverpod позволял providers хранить собственное состояние, это означало бы, что что угодно в приложении могло бы читать и изменять это состояние. В таком случае было бы сложно контролировать, как и когда состояние изменяется.
В архитектуре Riverpod обновление состояния централизовано: вся логика изменения provider находится внутри самого provider. Обычно UI вызывает только один метод у Notifier данного provider.
Более удобное тестирование. Хранение состояния providers внутри контейнера позволяет не заботиться о сбросе состояния между тестами. Достаточно создавать новый контейнер для каждого теста — и для каждого provider будет формироваться новое, "чистое" состояние.
test('Counter starts at 0 and can be incremented', () {
final container = ProviderContainer.test();
expect(container.read(counterProvider), 0);
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
});
test('Counter cannot go below 0', () {
final container = ProviderContainer.test();
expect(container.read(counterProvider), 0);
container.read(counterProvider.notifier).decrement();
expect(container.read(counterProvider), 0);
});В примере выше видно, что оба теста используют один и тот же provider, но изменения состояния в одном тесте не влияют на другой.
То же самое относится и к использованию ProviderScope и тестированию виджетов.
Централизованное место настройки приложения.
Через ProviderContainer и ProviderScope, можно настраивать различные аспекты приложения на уровне всего Riverpod. Например:- Можно определить кастомный ProviderObserver, чтобы отслеживать все изменения состояния в приложении. См. ProviderObservers.
- Можно переопределять providers локально или глобально — это полезно для тестирования, разных окружений или разработки. См. Provider overrides.
Поддержка Scoping providers. Так как состояние provider хранится внутри контейнера, один и тот же provider может возвращать разные значения в зависимости от того, где именно в дереве виджетов он используется. Это довольно продвинутая возможность и обычно не рекомендуется к использованию без необходимости, но может быть полезна для оптимизации производительности.