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

Family

Одна из самых мощных возможностей Riverpod называется Family.
Проще говоря, она позволяет связывать provider с несколькими независимыми состояниями, зависящими от уникальной комбинации параметров.

Типичный пример использования — получение данных из удалённого API, где ответ зависит от параметров (например, ID пользователя, поисковый запрос или номер страницы). Это позволяет определить один provider, который можно использовать для получения и кэширования данных для любой комбинации параметров.

Graph showing how family links a provider to multiple independent states

к сведению

Если обычные providers можно представить как переменную, то family можно представить как Map.

Создание Family

Определение family выполняется путём небольшого изменения объявления provider — теперь он принимает параметр.

Для функциональных providers синтаксис выглядит следующим образом:

// При использовании без 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);
});

Для notifier providers синтаксис выглядит следующим образом:

// Для 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);
}
}
к сведению

Хотя это и не является строго обязательным, настоятельно рекомендуется включать 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(),
    ),
    );