Family
Одна из самых мощных возможностей Riverpod называется Family.
Проще говоря, она позволяет связывать provider с несколькими независимыми состояниями,
зависящими от уникальной комбинации параметров.
Типичный пример использования — получение данных из удалённого API, где ответ зависит от параметров (например, ID пользователя, поисковый запрос или номер страницы). Это позволяет определить один provider, который можно использовать для получения и кэширования данных для любой комбинации параметров.
Если обычные providers можно представить как переменную, то family можно представить как Map.
Создание Family
Определение family выполняется путём небольшого изменения объявления provider — теперь он принимает параметр.
Для функциональных providers синтаксис выглядит следующим образом:
- riverpod
- riverpod_generator
// При использовании без code-generation, providers могут использовать ".family".
// Это добавляет один generic-параметр, соответствующий типу параметра.
// Функция инициализации затем получает этот параметр.
final userProvider = FutureProvider.autoDispose.family<User, String>((
ref,
id,
) async {
final dio = Dio();
final response = await dio.get('https://api.example.com/users/$id');
return User.fromJson(response.data);
});
Future<User> user(
Ref ref,
// При использовании code-generation providers могут принимать любое количество параметров.
// Они могут быть как позиционными/именованными, так и обязательными/необязательными.
String id,
) async {
final dio = Dio();
final response = await dio.get('https://api.example.com/users/$id');
return User.fromJson(response.data);
}
Для notifier providers синтаксис выглядит следующим образом:
- riverpod
- riverpod_generator
// Для notifier providers мы также используем ".family" и добавляем дополнительный
// generic-параметр.
// Главное отличие в том, что связанный Notifier должен определить конструктор
// и поле для получения этого параметра.
final userProvider = AsyncNotifierProvider.autoDispose
.family<UserNotifier, User, String>(UserNotifier.new);
class UserNotifier extends AsyncNotifier<User> {
// Сохраняем аргумент в поле, чтобы затем его использовать
UserNotifier(this.id);
final String id;
Future<User> build() async {
final dio = Dio();
final response = await dio.get('https://api.example.com/users/$id');
return User.fromJson(response.data);
}
}
class UserNotifier extends _$UserNotifier {
Future<User> build(
// При использовании code-generation Notifier'ы могут принимать параметры
// прямо в методе build. Можно определить любое количество параметров.
String id,
) async {
final dio = Dio();
final response = await dio.get('https://api.example.com/users/$id');
// Сгенерированный класс также получает доступ к параметрам build
// через `this`:
print(this.id);
return User.fromJson(response.data);
}
}
Хотя это и не является строго обязательным, настоятельно рекомендуется включать Automatic disposal
при использовании family.
Это помогает избежать утечек памяти в ситуациях, когда параметр меняется, а предыдущее состояние больше не нужно.
Использование Family
Providers, которые принимают параметры, также немного меняют способ использования.
Проще говоря, нужно передавать параметры, которые ожидает provider:
final user = ref.watch(userProvider('123'));
Передаваемые параметры должны иметь корректную и стабильную реализацию == и hashCode.
Можно рассматривать "family" как Map, где параметры — это ключ, а состояние provider — значение.
Поэтому если ==/hashCode параметра ведут себя некорректно, вы можете получать разные состояния для "одинаковых" входных данных.
Из-за этого следующий код считается некорректным:
// Неправильный параметр, так как `[1, 2, 3] != [1, 2, 3]`
ref.watch(myProvider([1, 2, 3]));
Чтобы легче находить такие ошибки, рекомендуется использовать riverpod_lint и включить правило provider_parameters. Тогда подобные случаи будут подсвечиваться как предупреждения Подробнее см. в разделе установки Getting started.
Вы можете использовать сколько угодно family — все они будут независимыми. Например:
class Example extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final user1 = ref.watch(userProvider('123'));
final user2 = ref.watch(userProvider('456'));
// user1 и user2 независимы
}
}
Доступ ко всем активным provider'ам family
Используя ProviderContainers/ProviderScopes, можно получить доступ ко всем
provider'ам, связанным с Family, с помощью метода allProviders:
for (final reference in ref.container.allProviders(family: userProvider)) {
ref.invalidate(reference.provider);
}
Переопределение family
При попытке замокать provider в тестах может потребоваться переопределить family provider.
В этом случае есть два варианта:
- Переопределить только конкретную комбинацию параметров:
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider('123').overrideWith((ref) => User(name: 'User 123')),
],
child: const MyApp(),
),
); - Переопределить все комбинации параметров:
await tester.pumpWidget(
ProviderScope(
overrides: [
userProvider.overrideWith((ref, arg) => User(name: 'User $arg')),
],
child: const MyApp(),
),
);