Scoped DI
In this library, scoped dependency injection (DI) refers to injecting providers that are confined to a specific scope.
To make things clearer in this section, let’s take a look at two of the providers from the previous page.
final numberProvider = Provider((context) => 5);
final doubleNumberPlusArgProvider = Provider.withArgument((context, int arg) { final number = numberProvider.of(context); return number * 2 + arg;});How to scope
Scoping means that the provider must be specified within a ProviderScope before it can be injected.
In case the provider does not take an argument, we scope it the following way:
ProviderScope( providers: [numberProvider()] child: // ...)In case the provider takes an argument, we need to specify it when providing it.
ProviderScope( providers: [doubleNumberPlusArgProvider(10)] child: // ...)How to inject
Injecting is the act of retrieving a dependency. It is done with the methods of(context) and maybeOf(context), the latter one being safer because it returns null instead of throwing if the provider is not found in any scopes.
In full example below, we are going to inject the two providers above with numberProvider.of(context) and doubleNumberPlusArgProvider.of(context).
Full example
Try and guess what the displayed text will be before reading the solution.
runApp( MaterialApp( home: Scaffold( body: ProviderScope( providers: [numberProvider(), doubleNumberPlusArgProvider(10)], child: Builder( builder: (context) { final number = numberProvider.of(context); final doubleNumberPlusArg = doubleNumberPlusArgProvider.of(context); return Text('$number $doubleNumberPlusArg'); }, ), ), ), ),);The solution is “5 20”.
Scoping correctly with context
Some providers might have a dependency on other providers. The context a provider receives in its create belongs to the ProviderScope providing it (it is placed right below that scope), and not to the widget injecting it. Therefore, a provider can inject:
- any provider declared in an ancestor
ProviderScope; - any provider declared in its own
ProviderScope, in any order.
A provider can never inject a provider declared in a ProviderScope placed below its own, even if it is first injected by a widget placed below that scope. This way, a value always depends on the same values, no matter where it is injected first, and it never depends on a value that may be disposed before it.
final countProvider = Provider.withArgument((context, int count) => count);final doubleCountProvider = Provider( (context) => countProvider.of(context) * 2,);
ProviderScope( providers: [countProvider(1), doubleCountProvider()], child: ProviderScope( providers: [countProvider(100)], child: Builder( builder: (context) { // 2, not 200: doubleCountProvider lives in the outer scope, therefore // it injects the countProvider of the outer scope. return Text('${doubleCountProvider.of(context)}'); }, ), ),)Inherited widgets in create
The same applies to any other inherited widget read in create, such as Theme.of(context), Navigator.of(context) or Localizations.of(context): they are resolved where the ProviderScope is placed, and not where the value is injected. A provider can only see the inherited widgets placed above its ProviderScope.
This matters especially for a ProviderScope placed above MaterialApp, since the Navigator, the Theme and the localizations of the app all live inside MaterialApp:
final themedLabelProvider = Provider( (context) => Label(color: Theme.of(context).primaryColor),);final routerProvider = Provider( (context) => AppRouter(Navigator.of(context)),);
// The ProviderScope is ABOVE MaterialApp.ProviderScope( providers: [themedLabelProvider(), routerProvider()], child: MaterialApp( theme: ThemeData(primaryColor: Colors.red), home: Builder( builder: (context) { themedLabelProvider.of(context); // gets the default color, not red routerProvider.of(context); // throws: no Navigator above the scope return const HomePage(); }, ), ),)There are two ways to fix it. Either place the ProviderScope below MaterialApp, so that its providers can see what MaterialApp provides:
MaterialApp( theme: ThemeData(primaryColor: Colors.red), // Placed below MaterialApp, so the providers see its Theme and Navigator. home: ProviderScope( providers: [themedLabelProvider(), routerProvider()], child: const HomePage(), ),)Or keep the value independent of the UI, and read the inherited widget where the value is used, i.e. in the build method of the widget:
final labelProvider = Provider((context) => Label());
// In a widget:final label = labelProvider.of(context);final color = Theme.of(context).primaryColor;The order in which the providers are declared does not fully matter, because all the providers of a ProviderScope are registered as soon as the scope is mounted, before any value is created. A value that is not created yet is created the first time it is injected, also while another value of the same scope is being created, no matter whether it is lazy or not. The order only determines in which order independent non-lazy values are created (and, therefore, disposed):
// Both of these work, and behave identically.ProviderScope( providers: [numberProvider(), doubleNumberPlusArgProvider(10)], child: // ...)
ProviderScope( providers: [doubleNumberPlusArgProvider(10), numberProvider()], child: // ...)Circular dependencies
The only combination that cannot work is a cycle, i.e. a provider that directly or indirectly injects itself. This is detected while the value is being created and reported with a ProviderCircularDependencyError, which lists the providers taking part in the cycle:
// Throws a ProviderCircularDependencyError the first time either provider is// injected.final aProvider = Provider((context) => bProvider.of(context) + 1);final bProvider = Provider((context) => aProvider.of(context) + 1);Disposal order
Because a value that is injected and not created yet is created on the fly (no matter whether it is lazy or not), a value is always created after the values it depends on. When a ProviderScope is disposed, its values are therefore disposed in the reverse order of creation, so that a value is always disposed before its own dependencies. This means that the dispose of a provider can safely use the values it injected in its create. The values injected from an ancestor ProviderScope are safe as well, since an ancestor scope is always disposed after its descendants.
Graphical representation
When you inject a provider, you need to ensure that one of the ancestors of the widget — where the injection takes place — is a ProviderScope providing that provider. If this is not the case, an error will be thrown at runtime.
Refer to the graph below to understand the problem.
If you lift the provider scope up to the parent of the widget, the injection will work as expected.