family constant Null safety

StreamProviderFamilyBuilder const family

A group of providers that builds their value from an external parameter.

Families can be useful to connect a provider with values that it doesn't have access to. For example:

  • Allowing a "title provider" access the Locale

    final titleFamily = Provider.family<String, Locale>((ref, locale) {
      if (locale == const Locale('en')) {
        return 'English title';
      } else if (locale == const Locale('fr')) {
        return 'Titre Français';
      }
    });
    
    // ...
    
    @override
    Widget build(BuildContext context, WidgetRef ref) {
      final locale = Localizations.localeOf(context);
    
      // Obtains the title based on the current Locale.
      // Will automatically update the title when the Locale changes.
      final title = ref.watch(titleFamily(locale));
    
      return Text(title);
    }
    
  • Have a "user provider" that receives the user ID as parameter

    final userFamily = FutureProvider.family<User, int>((ref, userId) async {
      final userRepository = ref.read(userRepositoryProvider);
      return await userRepository.fetch(userId);
    });
    
    // ...
    
    @override
    Widget build(BuildContext context, WidgetRef ref) {
      int userId; // Read the user ID from somewhere
    
      // Read and potentially fetch the user with id `userId`.
      // When `userId` changes, this will automatically update the UI
      // Similarly, if two widgets tries to read `userFamily` with the same `userId`
      // then the user will be fetched only once.
      final user = ref.watch(userFamily(userId));
    
      return user.when(
        data: (user) => Text(user.name),
        loading: () => const CircularProgressIndicator(),
        error: (err, stack) => const Text('error'),
      );
    }
    
  • Connect a provider with another provider without having a direct reference on it.

    final repositoryProvider = Provider.family<String, FutureProvider<Configurations>>((ref, configurationsProvider) {
      // Read a provider without knowing what that provider is.
      final configurations = await ref.read(configurationsProvider.future);
      return Repository(host: configurations.host);
    });
    

Usage

The way families works is by adding an extra parameter to the provider. This parameter can then be freely used in our provider to create some state.

For example, we could combine family with FutureProvider to fetch a Message from its ID:

final messagesFamily = FutureProvider.family<Message, String>((ref, id) async {
  return dio.get('http://my_api.dev/messages/$id');
});

Then, when using our messagesFamily provider, the syntax is slightly modified. The usual:

Widget build(BuildContext context, WidgetRef ref) {
  // Error – messagesFamily is not a provider
  final response = ref.watch(messagesFamily);
}

will not work anymore. Instead, we need to pass a parameter to messagesFamily:

Widget build(BuildContext context, WidgetRef ref) {
  final response = ref.watch(messagesFamily('id'));
}

NOTE: It is totally possible to use a family with different parameters simultaneously. For example, we could use a titleFamily to read both the french and english translations at the same time:

@override
Widget build(BuildContext context, WidgetRef ref) {
  final frenchTitle = ref.watch(titleFamily(const Locale('fr')));
  final englishTitle = ref.watch(titleFamily(const Locale('en')));

  return Text('fr: $frenchTitle en: $englishTitle');
}

Parameter restrictions

For families to work correctly, it is critical for the parameter passed to a provider to have a consistent hashCode and ==.

Ideally the parameter should either be a primitive (bool/int/double/String), a constant (providers), or an immutable object that override == and hashCode.

  • PREFER using family in combination with autoDispose if the parameter passed to providers is a complex object:

    final example = Provider.autoDispose.family<Value, ComplexParameter>((ref, param) {
    });
    

    This ensures that there is no memory leak if the parameter changed and is never used again.

Passing multiple parameters to a family

Families have no built-in support for passing multiple values to a provider.

On the other hand, that value could be anything (as long as it matches with the restrictions mentioned previously).

This includes:

  • A tuple (using package:tuple)
  • Objects generated with Freezed/built_value
  • Objects based on package:equatable

This includes:

  • A tuple (using package:tuple)

  • Objects generated with Freezed/built_value, such as:

    @freezed
    abstract class MyParameter with _$MyParameter {
      factory MyParameter({
        required int userId,
        required Locale locale,
      }) = _MyParameter;
    }
    
    final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
      print(myParameter.userId);
      print(myParameter.locale);
      // Do something with userId/locale
    });
    
    @override
    Widget build(BuildContext context, WidgetRef ref) {
      int userId; // Read the user ID from somewhere
      final locale = Localizations.localeOf(context);
    
      final something = ref.watch(
        exampleProvider(MyParameter(userId: userId, locale: locale)),
      );
    }
    
  • Objects based on package:equatable, such as:

    class MyParameter extends Equatable  {
      factory MyParameter({
        required this.userId,
        requires this.locale,
      });
    
      final int userId;
      final Local locale;
    
      @override
      List<Object> get props => [userId, locale];
    }
    
    final exampleProvider = Provider.family<Something, MyParameter>((ref, myParameter) {
      print(myParameter.userId);
      print(myParameter.locale);
      // Do something with userId/locale
    });
    
    @override
    Widget build(BuildContext context, WidgetRef ref) {
      int userId; // Read the user ID from somewhere
      final locale = Localizations.localeOf(context);
    
      final something = ref.watch(
        exampleProvider(MyParameter(userId: userId, locale: locale)),
      );
    }
    

Implementation

static const family = StreamProviderFamilyBuilder();