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 is the context of 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.
The order in which the providers are declared does not matter, because all the providers of a ProviderScope are registered as soon as the scope is mounted, while their values are only created on demand:
// 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 the values are created lazily, 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.
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.