Provider retrieval process
Refer to the following graph to understand how the providers are retrieved.
Steps
-
When a provider is injected, the first
ProviderScopeancestor is searched. -
If a
ProviderScopeancestor is found, the provider is searched in its internal map of providers. -
If the provider is found, its value is returned. If the value has not been created yet (i.e. it is the first time the provider is injected), it gets created right before it is returned.
-
If the provider is not found, the search proceeds to the next
ProviderScopeancestor, continuing recursively up the widget tree until the root is reached. -
If the provider is not found, a
ProviderWithoutScopeErroris thrown.
A ProviderScopeOverride needs no special treatment in this process: it holds an internal ProviderScope providing the mocks, which takes part in the traversal like any other scope.
The three layers of providers
The steps above talk about “the internal map of providers” of a scope. Knowing what that map actually contains makes the behavior of overrides — and of providers with an argument — easy to predict.
-
Top-level providers. These are the providers you declare in your files. They are never used to create anything: they only act as type-safe identifiers. This is why they can be declared globally without holding any global state.
-
Intermediate providers. Whenever a top-level provider is inserted into a
ProviderScope, that scope generates an intermediate provider for it and stores the pair in its internal map. The intermediate provider is the one actually responsible for creating (and disposing) the value. -
Values. These are the objects your widgets inject, and they are stored per intermediate provider.
The second layer is what makes overrides and arguments possible, because an intermediate provider can be regenerated from something else than the top-level provider it is registered under:
| Inserted provider | Intermediate provider |
|---|---|
myProvider() | myProvider itself |
myProvider(), with an override | a copy of the provider passed to overrideWith |
myArgProvider(arg) | a provider combining myArgProvider with arg |
myArgProvider(arg), with an override | a provider combining the provider passed to overrideWith with arg |
Since the values are keyed by their intermediate provider, and since a fresh intermediate provider is generated for every override, the same mock can be reused to override several providers without the resulting values being shared.
How overrides are applied
A ProviderScopeOverride merely registers its overrides. Every ProviderScope below it consults that registry while generating its own intermediate providers, so that the value of a mock lives exactly where the value of the original provider would have lived — and therefore shares its lifecycle.
There is one exception, and it concerns argument providers only: an argument provider cannot be instantiated by the ProviderScopeOverride itself, since no argument is available there. A plain Provider can, which is why a plain provider can be overridden even when no ProviderScope provides it at all.