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

Scopes

предупреждение

The content of this page may be outdated.
It will be updated in the future, but for now you may want to refer to the content in the top of the sidebar instead (introduction/essentials/case-studies/...)

Scoping in Riverpod is a very powerful feature, but like all powerful features, it should be used wisely and intentionally.

A few of the things that scoping enables are:

  • Override the state of providers for a specific subtree (similar to how theming and InheritedWidgets work in flutter) (see example)
  • Creating synchronous providers for normally async APIs (see example)
  • Allowing Dialogs and Overlays to inherit the state of providers from the widget subtree that cause them to be shown (see example)
  • Optimizing rebuilds of widgets by removing parameters from Widget constructors allowing you to make them const

If you are wanting to use scope for the first point, chances are you can use families instead. Families have the advantages of allowing you to access each of those instances of the state from anywhere in the widget tree rather than just the state scoped to the specific subtree that you are in.

Using scope to create multiple instances of a provider's state is similar to how package:provider works.

However, using scope to accomplish that task, is more restrictive, as you cannot decide to access other instances from that scope.

As such, before scoping every provider you use, consider carefully why you want to scope the provider.

ProviderScope and ProviderContainer

A scope is introduced by a ProviderContainer. This container holds the current state of all of your providers. It manages the lookup and subscriptions between providers.

In Flutter you should use the ProviderScope widget, which contains a ProviderContainer internally, and provides a way to access that container to the rest of the widget tree.

final valueProvider = StateProvider((ref) => 0);

// DO this
void main() {
runApp(ProviderScope(child: MyApp()));
}

//DON'T do this:
final myProviderContainer = ProviderContainer();
void main(){
runApp(MyApp());
}
осторожно

Do not use multiple ProviderContainers, without an understanding of how they work. Each will have it's own separate thread of states, which will not be able to access each other. Tests are an example of when you might want to use separate ProviderContainers in order to make each test's state independent of the others.

Only create a ProviderContainer without a ProviderScope for testing and dart-only usage.

How Riverpod Finds a Provider

When a widget or provider requests the value of a provider, Riverpod looks up the state of that provider in the nearest ProviderScope widget. If neither the provider nor one of it's explicitly listed dependencies is overridden in that scope Riverpod continues it's lookup up the widget tree. If the provider has not been overridden in any Widget subtrees the lookup defaults to the ProviderContainer in the root ProviderScope.

Once this process locates the scope in which the provider should reside it determines if the provider has been created yet. If so, it will return the state of the provider. However, if the provider has been invalidated or is not currently initialized it will create the state using the provider's build method.

Initialization of Synchronous Provider for Async APIs

Often you might have some async initialization of a dependency such as SharedPreferences or FirebaseApp. Many other providers might rely on this, and dealing with the error / loading states in each of those providers is redundant.

You might be able to guarantee that those providers will not have errors and will load quickly when the app is started.

So how do you makes these sorts of provider states available synchronously?

Here is an example that shows how scoping allows you override a dummy provider when your asynchronous API is ready.

final countProvider = NotifierProvider<CountNotifier, int>(CountNotifier.new);

class CountNotifier extends Notifier<int> {

int build() {
// We'd like to obtain an instance of shared preferences synchronously in a provider
final preferences = ref.watch(sharedPreferencesProvider);
final currentValue = preferences.getInt('count') ?? 0;
listenSelf((prev, next) {
preferences.setInt('count', next);
});
return currentValue;
}

void increment() => state++;
}

// We don't have an actual instance of SharedPreferences, and we can't get one except asynchronously
final sharedPreferencesProvider =
Provider<SharedPreferences>((ref) => throw UnimplementedError());

Future<void> main() async {
// Show a loading indicator before running the full app (optional)
// The platform's loading screen will be used while awaiting if you omit this.
runApp(const LoadingScreen());

// Get the instance of shared preferences
final prefs = await SharedPreferences.getInstance();
return runApp(
ProviderScope(
overrides: [
// Override the unimplemented provider with the value gotten from the plugin
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: const MyApp(),
),
);
}

class MyApp extends ConsumerWidget {
const MyApp({super.key});


Widget build(BuildContext context, WidgetRef ref) {
// Use the provider without dealing with async issues
final count = ref.watch(countProvider);
return Text('$count');
}
}

Subtree Scoping

Scoping allows you to override the state of a provider for a specific subtree of your widget tree. In this way it can provide a similar mechanism to InheritedWidget from flutter, or the providers from package:provider.

For example, in flutter you can override the Theme for a particular subtree of your widget tree, by wrapping it in a Theme widget.


void main() {
runApp(
ProviderScope(
child: MaterialApp(
theme: ThemeData(primaryColor: Colors.blue),
home: const Home(),
),
),
);
}

// Have a counter that is being incremented
final counterProvider = StateProvider(
(ref) => 0,
);

class Home extends ConsumerWidget {
const Home({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
// This counter will have a primary color of green
Theme(
data: Theme.of(context).copyWith(primaryColor: Colors.green),
child: const CounterDisplay(),
),
// This counter will have a primary color of blue
const CounterDisplay(),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
));
}
}

class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$count',
style: theme.textTheme.displayMedium
?.copyWith(color: theme.primaryColor),
),
],
);
}
}

Under the hood, Theme is an InheritedWidget and when widgets look up the Theme they get the Theme from the nearest Theme widget above it in the widget tree.

Riverpod works differently, since all of the state of your application is typically stored in a root ProviderScope widget. Don't worry, this doesn't cause your whole application to rebuild when the state changes, it just allows you to access the state from anywhere in your widget tree.

What if you want different providers depending on which page you are in?

The first thing that you should consider is whether the provided behavior will differ in any way.

If so -> just create a new provider with a different name and use it in that page

If not -> Consider using a .family.

Often you start by thinking that you only need a provider on a particular page, but end up wanting to use it in another page later on. Families protect you against this eventuality, and are a major difference in how you should adjust your thinking if you are coming from package:provider.

If families really do not fit your use case, the following example shows you how to override a provider for a particular subtree.


/// A counter that is being incremented by each [CounterDisplay]'s ElevatedButton
final counterProvider = StateProvider(
(ref) => 0,
);

final adjustedCountProvider = Provider(
(ref) => ref.watch(counterProvider) * 2,
// Note that if a provider depends on a provider that is overridden for a subtree,
// you must explicitly list that provider in your dependencies list.
dependencies: [counterProvider],
);

class Home extends ConsumerWidget {
const Home({super.key});


Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Column(
children: [
ProviderScope(
/// Just specify which provider you want to have a copy of in the subtree
///
/// Note that dependant providers such as [adjustedCountProvider] will
/// also be copied for this subtree. If that is not the behavior you want,
/// consider using families instead
overrides: [counterProvider],
child: const CounterDisplay(),
),
ProviderScope(
// You can change the provider's behavior in a particular subtree
overrides: [counterProvider.overrideWith((ref) => 1)],
child: const CounterDisplay(),
),
ProviderScope(
overrides: [
counterProvider,
// You can also change dependent provider's behaviors
adjustedCountProvider.overrideWith(
(ref) => ref.watch(counterProvider) * 3,
),
],
child: const CounterDisplay(),
),
// This particular display will use the provider state from the root ProviderScope
const CounterDisplay(),
],
));
}
}

class CounterDisplay extends ConsumerWidget {
const CounterDisplay({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('$count'),
ElevatedButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
child: const Text('Increment Count'),
),
],
);
}
}

When to choose Scoped Providers or Families

While scopes are important to understand, it is easy to get carried away when using scopes.

If you want a different instance of a provider's state depending on where it is in the widget tree you have a few alternatives available to you: Scoping, Families, or a combination. The appropriate choice depends on your use case.

Families:

  • Pro: You can show multiple of the states no matter which subtree you are in
  • Pro: This makes it a more flexible and scalable solution for many use cases

Scoping:

  • Con: You end up with more nesting of ProviderScope widgets in your widget tree
  • Con: You can only access the one override in your section of the widget tree
  • Con: You end up having to explicitly list the dependencies of most of your providers
  • Pro: You can reduce the number of parameters in your widget constructors
  • Pro: You get a slight performance advantage, and can potentially make some of your widget constructors const

Using a combination of the two approaches, you can get the pros of both approaches, but you still have to deal with the cons of scoping.

осторожно

Remember that scopes introduce a new instance of the state of every provider that is overridden or has listed a dependency on a provider that was overridden. If you override with the same parameter in a different subtree of the app, it will not be the same instance of the provider's state. Families are more flexible in general, and with the upcoming code generation feature it is easy to use multiple parameters for a family. Often a good combination is to use both families and scoping. Use a family to provide general access to a piece of state anywhere in your app, and then use scoping to provide a specific instance of the family's state depending on where you are in the widget tree.

Less common usages of Scopes

Sometimes you may want to override a whole set of providers in a specific subtree of your app. By listing a common provider in the dependencies list of each of those providers, you can easily create new states for all of them at once, by overriding the common one.

Note that if you try to use families for this, you will end up with many families that all have the same parameter, and you could end up passing that parameter all over the widget tree. In this case it is also acceptable to use scopes.

осторожно

Once you start using scope, make sure to always list your dependencies and keep them up to date, to prevent runtime exceptions. To help with this we have created riverpod_lint which will warn you if there is a missing dependency. Additionally with riverpod_generator the code generator automatically generates the dependency list.