Providers
Providers are the central feature of Riverpod. If you use Riverpod, you use it for its providers.
What is a provider?
Providers are essentially "memoized functions", with sugar on top.
What this means is, providers are functions which will return a cached value
when called with the same parameters.
The most common use-case for using providers is to perform a network request.
Consider a function that fetches a user from an API:
Future<User> fetchUser() async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
One issue with this function is, if we were to try using it inside a widgets, we'd have to cache the result ourselves ; then figure out a way to share the value across all widgets that need it.
That is where providers come in. Providers are wrappers around functions. They will cache the result of said function and allow multiple widgets to access the same value:
- riverpod
- riverpod_generator
// The equivalent of our fetchUser function, but the result is cached.
// Using userProvider multiple times will return the same value.
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
});
// The equivalent of our fetchUser function, but the result is cached.
// This will generate a "userProvider". Using it multiple times will
// return the same value.
Future<User> user(Ref ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
On top of basic caching, providers then add various features to make them more powerful:
- Built-in cache invalidation mechanisms
In particular, Ref.watch allows you to combined caches together, automatically invalidating what is needed. - Automatic disposal
Providers can automatically release resources when they are no longer needed. - Data-binding
Providers remove the need for a FutureBuilder or a StreamBuilder. - Automatic error handling
Providers can automatically catch errors and expose them to the UI. - Mocking support
For better testing and other purposes, all providers can be mocked. See Provider overrides. - Offline persistence (experimental)
The result of a provider can be persisted to disk and automatically reloaded when the app is restarted. - Mutations (experimental)
Providers offer a built-in way for UIs render a spinner/error for side effects (such as form submission).
Providers come 6 variants:
Synchronous | Future | Stream | |
---|---|---|---|
Unmodifiable | Provider | FutureProvider | StreamProvider |
Modifiable | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
This may seem overwhelming at first. Let's break it down.
Sync vs Future vs Stream:
The columns of this table represent the built-in Dart types for functions.
int synchronous() => 0;
Future<int> future() async => 0;
Stream<int> stream() => Stream.value(0);
Unmodifiable vs Modifiable:
By default, providers are not modifiable by widgets. The "Notifier" variant of
providers make them externally modifiable.
This is similar to a private setter ("unmodifiable" providers)
// _state is internally modifiable
// but cannot be modified externally
var _state = 0;
int get state => _state;
VS a public setter ("modifiable" providers)
// Anything can modify "state"
var state = 0;
You can also view unmodifiable vs modifiable as respectively StatelessWidget
vs StatefulWidget
in principle.
This is not entirely accurate, as providers are not widgets and both kinds store "state". But the principle is similar: "One object, immutable" vs "Two objects, mutable".
Creating a provider
Providers should be created as "top-level" declarations.
This means that they should be declared outside of any class or function.
The syntax for creating a provider depends on whether it is "modifiable" or "unmodifiable", as per the table above.
- Unmodifiable (functional)
- Modifiable (notifier)
- riverpod
- riverpod_generator
final name = SomeProvider.someModifier<Result>((ref) { <your logic here> });
The provider variable | This variable is what will be used to interact with our provider. примечание Do not be frightened by the global aspect of providers. Providers are fully immutable. Declaring a provider is no different from declaring a function, and providers are testable and maintainable. |
The provider type | Generally either Provider, FutureProvider or StreamProvider.
подсказка Don't think in terms of "Which provider should I pick". Instead, think in terms of "What do I want to return". The provider type will follow naturally. |
Modifiers (optional) | Often, after the type of the provider you may see a "modifier". There are currently two modifiers available:
|
Ref | An object used to interact with other providers. |
The provider function | This is where we place the logic of our providers.
This function will be called when the provider is first read. |
@riverpod Result myFunction(Ref ref) { <your logic here> }
The annotation | All generated providers must be annotated with For example, we can disable "auto-dispose" (which we will see later) by writing |
The annotated function | The name of the annotated function determines how the provider
will be interacted with. Annotated functions must specify a Ref as first parameter. This function will be called when the provider is first read. |
Ref | An object used to interact with other providers. |
- riverpod
- riverpod_generator
final name = SomeNotifierProvider.someModifier<MyNotifier, Result>(MyNotifier.new); class MyNotifier extends SomeNotifier<Result> { @override Result build() { <your logic here> } <your methods here> }
The provider variable | This variable is what will be used to interact with our provider. примечание Do not be frightened by the global aspect of providers. Providers are fully immutable. Declaring a provider is no different from declaring a function, and providers are testable and maintainable. |
The provider type | Generally either NotifierProvider, AsyncNotifierProvider or StreamNotifierProvider. AsyncNotifierProvider is the one you'll want to use the most. подсказка As with functional providers, don't think in terms of "Which provider should I pick". Create whatever state you want to create, and the provider type will follow naturally. |
Modifiers (optional) | Often, after the type of the provider you may see a "modifier". There are currently two modifiers available:
|
The Notifier's constructor | The parameter of "notifier providers" is a function which is expected
to instantiate the "notifier". |
The Notifier | If This class is responsible for exposing ways to modify the state of the provider. предупреждение Do not put logic in the constructor of your notifier.
|
The Notifier type | The base class extended by your notifier should match that of the provider + "family", if used. Some examples would be:
|
The build method | All notifiers must override the This method should not be called directly. |
@riverpod class MyNotifier extends _$MyNotifier { @override Result build() { <your logic here> } <your methods here> }
The annotation | All providers must be annotated with For example, we can disable "auto-dispose" (which we will see later) by writing |
The Notifier | When a Notifiers are responsible for exposing ways to modify the state of the provider. предупреждение Do not put logic in the constructor of your notifier.
|
The build method | All notifiers must override the This method should not be called directly. |
You can declare as many providers as you want without limitations.
As opposed to when using package:provider
, Riverpod allows creating multiple
providers exposing a state of the same "type":
- riverpod
- riverpod_generator
final cityProvider = Provider((ref) => 'London');
final countryProvider = Provider((ref) => 'England');
String city(Ref ref) => 'London';
String country(Ref ref) => 'England';
The fact that both providers create a String
does not cause any problem.
Using providers
Providers are similar to widgets in that, on their own, they do nothing.
Similarly to how widgets are a description of the UI, providers are a description
of the state.
Surprisingly, a provider is fully stateless, and could be instantiated as const
if not for the fact that this would make the syntax a bit more verbose.
To use a provider, you need a separate object: ProviderContainer. See ProviderContainers/ProviderScopes for more information.
Long story short, before you can use a provider, wrap your Flutter applications in a ProviderScope:
void main() {
runApp(ProviderScope(child: MyApp()));
}
Once that is done, you will need to obtain a Ref to interact with your providers. See Refs for information about those.
In short, there are two ways to obtain a Ref:
- Providers naturally get access to one.
This is the first parameter of the provider function, or theref
property of a Notifier. This enables providers to communicate with each other. - The Widget tree will need special kind of widgets, called Consumers. Those widgets bridge the gap between the widget tree and the provider tree, by giving you a WidgetRef.
As example, consider a helloWorldProvider
that returns a simple string.
You could use it inside widgets like this:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, _) {
// Obtain the value of the provider
final helloWorld = ref.watch(helloWorldProvider);
// Use the value in the UI
return Text(helloWorld);
},
);
}
}