.family
Before reading this, consider reading about Providers and Reading a Provider.
In this part, we will talk in detail about the .family
provider modifier.
The .family
modifier has one purpose: Getting a unique provider based on external parameters.
Some common use-cases for family
would be:
- Combining FutureProvider with
.family
to fetch aMessage
from its ID - Passing the current
Locale
to a provider, so that we can handle translations
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 can 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');
});
When using our messagesFamily
provider, the syntax is slightly different.
The usual syntax will not work anymore:
Widget build(BuildContext context, WidgetRef ref) {
// Error – messagesFamily is not a provider
final response = ref.watch(messagesFamily);
}
Instead, we need to pass a parameter to messagesFamily
:
Widget build(BuildContext context, WidgetRef ref) {
final response = ref.watch(messagesFamily('id'));
}
It is 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:
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 overrides ==
and hashCode
.
PREFER using autoDispose
when the parameter is not constant:
You may want to use families to pass the input of a search field to your provider.
But that value can change often and never be reused.
This could cause memory leaks as, by default, a provider is never destroyed even
if no longer used.
Using both .family
and .autoDispose
fixes that memory leak:
final characters = FutureProvider.autoDispose.family<List<Character>, String>((ref, filter) async {
return fetchCharacters(filter: filter);
});
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 from tuple
- Objects generated with Freezed or built_value
- Objects using equatable
Here's an example of using Freezed or equatable for multiple parameters:
- Freezed
- Equatable
abstract class MyParameter with _$MyParameter {
factory MyParameter({
required int userId,
required Locale locale,
}) = _MyParameter;
}
final exampleProvider = Provider.autoDispose.family<Something, MyParameter>((ref, myParameter) {
print(myParameter.userId);
print(myParameter.locale);
// Do something with userId/locale
});
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)),
);
...
}
class MyParameter extends Equatable {
MyParameter({
required this.userId,
required this.locale,
});
final int userId;
final Locale locale;
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
});
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)),
);
...
}