Skip to content

Provider retrieval process

Refer to the following graph to understand how the providers are retrieved.

Graphical representation of provider scope providers retrieval behavior

Steps

  1. When a provider is injected, the first ProviderScope ancestor is searched.

  2. If a ProviderScope ancestor is found, the provider is searched in its internal map of providers.

  3. 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.

  4. If the provider is not found, the search proceeds to the next ProviderScope ancestor, continuing recursively up the widget tree until the root is reached.

  5. If the provider is not found, a ProviderWithoutScopeError is 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.

  1. 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.

  2. 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.

  3. 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 providerIntermediate provider
myProvider()myProvider itself
myProvider(), with an overridea copy of the provider passed to overrideWith
myArgProvider(arg)a provider combining myArgProvider with arg
myArgProvider(arg), with an overridea 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.