Skip to content

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:

  1. any provider declared in an ancestor ProviderScope;
  2. 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.

Graphical representation of provider scope exception

If you lift the provider scope up to the parent of the widget, the injection will work as expected.

Graphical representation of provider scope