diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
index aac18d85..cfa937a4 100644
--- a/docs/astro.config.mjs
+++ b/docs/astro.config.mjs
@@ -45,7 +45,6 @@ export default defineConfig({
'core/modals',
'core/testing',
'core/provider-retrieval-process',
- 'core/configuration',
],
},
{
diff --git a/docs/src/content/docs/core/configuration.md b/docs/src/content/docs/core/configuration.md
deleted file mode 100644
index d5cc1ae1..00000000
--- a/docs/src/content/docs/core/configuration.md
+++ /dev/null
@@ -1,19 +0,0 @@
----
-title: Configuration
-description: How to set up the application-wide configuration for Disco.
----
-
-Disco aims to be minimally opinionated. To customize the default configuration, set the desired preferences using `DiscoConfig` before calling `runApp`. For example:
-
-```dart
-DiscoConfig.lazy = false;
-runApp(
- // ...
-);
-```
-
-### All options
-
-| Option | Default | Description |
-| -------------- | ------- | ----------- |
-| `lazy` | true | The values of the providers provided in a `ProviderScope` are created lazily. |
diff --git a/docs/src/content/docs/core/immutability.mdx b/docs/src/content/docs/core/immutability.mdx
index 211df106..d07ccb57 100644
--- a/docs/src/content/docs/core/immutability.mdx
+++ b/docs/src/content/docs/core/immutability.mdx
@@ -82,7 +82,35 @@ The typical usage for a provider is to provide a value for a specific page or wi
When the page or widget is disposed, the provider gets also disposed, and the value is no longer available.
When the Page is opened again, a new instance of the provider is created, and the value is provided again.
-### Force the recreation of a provider
+## The set of providers is fixed
+
+The same reasoning applies to the `providers` list itself: it is read exactly once, when the `ProviderScope` is mounted. Inserting a provider into it, or removing one from it, on a later rebuild has no effect at all.
+
+Since this would silently surface much later as a `ProviderWithoutScopeError`, it is reported as an error in debug mode:
+
+```dart
+// Reports an error in debug mode as soon as `showDetails` changes, and
+// continues with the original providers.
+ProviderScope(
+ providers: [
+ modelProvider(),
+ if (showDetails) detailsProvider(),
+ ],
+ child: // ...
+)
+```
+
+
+ Provide a fixed set of providers, and put the conditional ones in a nested `ProviderScope` that is itself inserted conditionally. That way, their values are created when the scope appears and disposed when it goes away.
+
+
+Note that only the *identity* of the providers is checked. Giving an argument provider a different argument on a rebuild remains allowed — and keeps being ignored, as shown in the example above.
+
+
+ The error is *reported* and not thrown: the `ProviderScope` keeps working with the providers it was mounted with. The check also only runs in debug mode; in release builds, a changed list is silently ignored, exactly as before.
+
+
+## Force the recreation of a provider
There is one way to force the recreation of a provider, _which we discourage_, but can be used if you know what you are doing.
diff --git a/docs/src/content/docs/core/modals.mdx b/docs/src/content/docs/core/modals.mdx
index d5fdef91..44a79528 100644
--- a/docs/src/content/docs/core/modals.mdx
+++ b/docs/src/content/docs/core/modals.mdx
@@ -46,7 +46,7 @@ runApp(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider],
+ providers: [numberProvider()],
child: Builder(
builder: (context) {
return ElevatedButton(
diff --git a/docs/src/content/docs/core/provider-retrieval-process.mdx b/docs/src/content/docs/core/provider-retrieval-process.mdx
index e8d820bb..9c407976 100644
--- a/docs/src/content/docs/core/provider-retrieval-process.mdx
+++ b/docs/src/content/docs/core/provider-retrieval-process.mdx
@@ -12,26 +12,53 @@ Refer to the following graph to understand how the providers are retrieved.
### Steps
-1. When a provider is injected, the first thing that is checked is if a `ProviderScopeOverride` exists.
+1. When a provider is injected, the first `ProviderScope` ancestor is searched.
-2. If it does, its internal map of overridden providers is checked to see if the provider is there.
+2. If a `ProviderScope` ancestor is found, the provider is searched in its internal map of providers.
-3. If is there, the overridden value is returned.
+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. Otherwise, the search continues for the first `ProviderScope` ancestor.
+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 a `ProviderScope` ancestor is found, the provider is searched in its internal map of providers.
+5. If the provider is not found, a `ProviderWithoutScopeError` is thrown.
-6. If the provider is found, its value is returned. It the value was not computed yet (i.e. it is the first time a lazy provider is accessed), it will get created right before it is returned.
-
-7. 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.
-
-8. 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 error can be avoided by using `provider.maybeOf(context)`, which returns `null` if the provider is not found.
-The lookup of a provider is __O(1)__ because it just involves a map lookup, in addition the tree is traversed using [getElementForInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/getElementForInheritedWidgetOfExactType.html), which is O(1) because it jumps from one InheritedWidget of type `T` to another, skipping the widgets in between.
+The lookup of a provider within each ProviderScope is __O(1)__ because it just involves a map lookup. The tree traversal uses [getElementForInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/getElementForInheritedWidgetOfExactType.html), which is O(1) per step because it jumps from one InheritedWidget of type `T` to another, skipping the widgets in between. The overall worst case is therefore O(number of traversed ProviderScope ancestors) when a provider is not found until the root.
+
+
+## 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 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.
+
+
+ The practical consequences are described in the [Testing](https://disco.mariuti.com/core/testing/) page. The recommendation of placing the `ProviderScopeOverride` as the root widget of your tests makes them irrelevant.
diff --git a/docs/src/content/docs/core/providers.mdx b/docs/src/content/docs/core/providers.mdx
index 3e5923ca..3d19ad67 100644
--- a/docs/src/content/docs/core/providers.mdx
+++ b/docs/src/content/docs/core/providers.mdx
@@ -35,6 +35,10 @@ class MyDatabase {
While providers can be declared globally, they **do not function globally**. They are just used as **identifiers** when registered in a scope.
+
+ As a consequence, the `create` function above is never called directly by you: a provider is called (i.e. `numberProvider()`) only to insert it into a `ProviderScope`, which is what creates and owns the value. See [Scoped DI](https://disco.mariuti.com/core/scoped-di/).
+
+
### Injection of other providers with context
Providers can leverage the context to inject other providers. The context will be relative to the scope in which they are provided.
@@ -77,7 +81,70 @@ final doubleNumberPlusArgProvider = Provider.withArgument((context, int arg) {
});
```
-## Dispose and lazy parameters
+## Lazy creation of the values
+
+The value of a provider is **always created lazily**: the `create` function is called the first time the provider is injected, and never before. If a provider is never injected, its value is never created.
+
+
+ This keeps the rule simple: a value exists if, and only if, something injected it. It is also the same behavior as Riverpod, which likewise has no way to flag a provider as eager.
+
+
+### Creating a value as soon as a scope is mounted
+
+Sometimes a value has to exist even if no widget injects it yet, e.g. because it starts a subscription or warms up a cache. In that case, inject it in a widget placed **below** the scope providing it:
+
+```dart
+ProviderScope(
+ providers: [engineProvider(), userProvider(42)],
+ child: Builder(
+ builder: (context) {
+ engineProvider.of(context); // created here
+ userProvider.of(context); // works for argument providers too
+ return const MyPage();
+ },
+ ),
+)
+```
+
+The `Builder` is what makes this work: its context is a descendant of the `ProviderScope`, therefore the injection finds it. The value is created only once, even though the builder may run again.
+
+If you prefer the injection to happen exactly once per mount, do it in the `initState` of a widget placed below the scope. This is safe, since `of(context)` does not make the widget depend on the provider:
+
+```dart
+class EagerProviders extends StatefulWidget {
+ const EagerProviders({required this.child, super.key});
+
+ final Widget child;
+
+ @override
+ State createState() => _EagerProvidersState();
+}
+
+class _EagerProvidersState extends State {
+ @override
+ void initState() {
+ super.initState();
+ engineProvider.of(context);
+ }
+
+ @override
+ Widget build(BuildContext context) => widget.child;
+}
+```
+
+
+ Injecting from a context which is **not** below the scope (for example in the widget that creates the `ProviderScope` itself, or in its `initState`) does not work: that context cannot see the scope, therefore a `ProviderWithoutScopeError` is thrown.
+
+
+
+ Keep the `child` in a variable (or `const`), so that the widget performing the eager injection can be rebuilt without rebuilding the subtree below it.
+
+
+
+ Both approaches create the value while the widget tree is being built. If your `create` function notifies some listener, postpone the notification (e.g. with `WidgetsBinding.instance.addPostFrameCallback`), otherwise Flutter complains that a widget was marked as needing to be rebuilt during a build.
+
+
+## Optional parameters
When defining a provider, we need to pass the positional `create` argument, which is a function used to generate the value contained by the provider.
@@ -86,4 +153,24 @@ There are also two optional named parameters that can be specified.
| Parameter | Default | Description |
| -------------- | ------- | ----------- |
| `dispose` | null | The function to call when the scope containing the provider gets disposed. It is used to dispose correctly the value held by the provider. |
-| `lazy` | `DiscoConfig.lazy`, which defaults to true | The provider's value is created lazily, meaning it is only created when first injected.|
+| `debugName` | null | An optional name, shown in the error messages of this library, which makes a provider easier to recognize. |
+
+## Overriding a provider
+
+A provider can be replaced by another provider of the same type through `overrideWith`, which is meant to be used for testing:
+
+```dart
+final myProviderOverride = myProvider.overrideWith(
+ Provider((context) => MyMock()),
+);
+```
+
+The same works for providers with an argument, as long as the mock takes an argument of the same type:
+
+```dart
+final myArgProviderOverride = myArgProvider.overrideWith(
+ Provider.withArgument((context, int arg) => MyMock(arg)),
+);
+```
+
+Since the override is a provider itself, it also controls how the mocked value is created and disposed. Refer to the [Testing](https://disco.mariuti.com/core/testing/) page to see how overrides are inserted into the widget tree.
diff --git a/docs/src/content/docs/core/scoped-di.mdx b/docs/src/content/docs/core/scoped-di.mdx
index c782b235..486e25c4 100644
--- a/docs/src/content/docs/core/scoped-di.mdx
+++ b/docs/src/content/docs/core/scoped-di.mdx
@@ -28,7 +28,7 @@ In case the provider does not take an argument, we scope it the following way:
```dart
ProviderScope(
- providers: [numberProvider]
+ providers: [numberProvider()]
child: // ...
)
```
@@ -42,6 +42,14 @@ ProviderScope(
)
```
+
+ Note the parentheses: a provider is never inserted into a `ProviderScope`
+ directly. Calling a provider returns the binding used to register it with
+ the scope. The provider value is created lazily, i.e. the first time it is
+ injected. Calling a provider is also what allows an argument to be passed.
+ This way, the syntax is the same for both kinds of providers.
+
+
### 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.
@@ -65,7 +73,7 @@ runApp(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider, doubleNumberPlusArgProvider],
+ providers: [numberProvider(), doubleNumberPlusArgProvider(10)],
child: Builder(
builder: (context) {
final number = numberProvider.of(context);
@@ -83,37 +91,45 @@ The solution is "5 20".
## Scoping correctly with context
-Some providers might have a dependency on other providers, to handle such cases, you can:
-1. Access another provider if it is declared in an ancestor `ProviderScope`.
-2. Declare the dependent provider after the provider it depends on, within the same `ProviderScope`. The order of declaration matters here otherwise a `ProviderForwardReferenceError` will be thrown at runtime.
+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:
-### Wrong example
+1. any provider declared in an ancestor `ProviderScope`;
+2. any provider declared in its own `ProviderScope`, in any order.
-The provider `doubleNumberPlusArgProvider` depends on `numberProvider`. Therefore, when scoping them both in the same `ProviderScope`, `numberProvider` must be declared first.
-Therefore, the following code will lead to a `ProviderForwardReferenceError`:
+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:
-```dart title="Wrong example"
+```dart
+// Both of these work, and behave identically.
ProviderScope(
- providers: [
- doubleNumberPlusArgProvider(10),
- numberProvider,
- ],
+ providers: [numberProvider(), doubleNumberPlusArgProvider(10)],
child: // ...
)
-```
-
-The error can be prevented by reordering the list of providers:
-```dart title="Correct example"
ProviderScope(
- providers: [
- numberProvider,
- doubleNumberPlusArgProvider(10),
- ],
+ 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:
+
+```dart
+// 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);
+```
+
+
+ A cycle usually means that a dependency is being resolved too early. Injecting the value where it is *used* instead of where it is *created* is often enough to break it.
+
+
+### 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.
diff --git a/docs/src/content/docs/core/testing.mdx b/docs/src/content/docs/core/testing.mdx
index 4bf0c886..dfc23f16 100644
--- a/docs/src/content/docs/core/testing.mdx
+++ b/docs/src/content/docs/core/testing.mdx
@@ -5,7 +5,11 @@ description: How to use overrides for testing.
import { Aside } from '@astrojs/starlight/components';
-Testing is done with overrides. You need to place a `ProviderScopeOverride` and then specify the `overrides` argument with a list containing the providers followed by `.overrideWithValue(T value)`.
+Testing is done with overrides. You need to place a `ProviderScopeOverride` and then specify the `overrides` argument with a list containing the providers followed by `.overrideWith(provider)`.
+
+An override replaces a provider **entirely**, and not just the value it holds: the provider passed to `overrideWith` is a regular provider, with its own `create` and `dispose`. The replacement must use the same provider kind as the provider being overridden: use `Provider.withArgument` to override a provider created with `Provider.withArgument`. This means a mock can, for instance, inject other providers through its context, exactly like the provider it replaces. The value of the original provider is never created at all.
+
+An override also takes the place of the provider it replaces in every `ProviderScope` below the `ProviderScopeOverride`. The mock therefore has the very same lifecycle as the original provider: its value is created lazily where the value of the original provider would have been created, it is disposed when that `ProviderScope` is disposed, and there is one value per `ProviderScope` providing it.
The overrides always take precedence over the providers provided with `ProviderScope`, even if the `ProviderScopeOverride` is placed above in the widget tree.
@@ -31,12 +35,12 @@ testWidgets(
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(100),
+ numberProvider.overrideWith(Provider((context) => 100)),
],
child: MaterialApp(
home: ProviderScope(
providers: [
- numberProvider,
+ numberProvider(),
],
child: Builder(
builder: (context) {
@@ -52,7 +56,7 @@ testWidgets(
});
```
-Testing is possible also with providers that take an argument, and it is done the same exact way.
+Testing is possible also with providers that take an argument, and it is done the same exact way. The only difference is that the mock is an argument provider as well, and thus it receives the argument that has been specified in the widget tree.
```dart
testWidgets(
@@ -62,7 +66,10 @@ testWidgets(
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(8),
+ // `arg` is 1 here, i.e. the argument passed to the provider below
+ numberProvider.overrideWith(
+ Provider.withArgument((context, int arg) => arg + 7),
+ ),
],
child: MaterialApp(
home: ProviderScope(
@@ -82,3 +89,55 @@ testWidgets(
expect(find.text('8'), findsOneWidget);
});
```
+
+### Mocking a class
+
+Since a mock is a full-fledged provider, the object it creates is built the same way as in production code. A typical override therefore looks like this:
+
+```dart
+class MockModel extends Model {
+ // ...
+}
+
+// ...
+
+modelProvider.overrideWith(Provider((context) => MockModel())),
+```
+
+
+ Note the explicit `Provider`: an override must have the exact same type as the provider it replaces. Without it, the type would be inferred as `Provider`, which does not compile.
+
+
+### Ignoring the value of a provider
+
+If a test does not care about the value of a provider, but the provider still has to be present, an override can also be used to make its creation cheap:
+
+```dart
+analyticsProvider.overrideWith(Provider((context) => NoopAnalytics())),
+```
+
+## Differences between the two kinds of overrides
+
+Overrides of providers and overrides of argument providers behave almost identically. The only difference comes from the fact that an argument provider can only be instantiated where its argument is known, i.e. where it is inserted into the widget tree:
+
+| | `Provider` | `Provider.withArgument` |
+| - | - | - |
+| Where the value lives | in the `ProviderScope` providing it | in the `ProviderScope` providing it |
+| Number of values created | one per `ProviderScope` providing it | one per `ProviderScope` providing it |
+| Requires a `ProviderScope` providing it | no | yes |
+
+A plain `Provider` does not strictly need a `ProviderScope` providing it, because the `ProviderScopeOverride` can provide the mock itself. This is handy when testing a single widget in isolation:
+
+```dart
+await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [modelProvider.overrideWith(Provider((_) => MockModel()))],
+ // No ProviderScope needed: the override provides the mock.
+ child: const MaterialApp(home: WidgetUnderTest()),
+ ),
+);
+```
+
+
+ An argument provider can never be overridden for a `ProviderScope` that is **not** a descendant of the `ProviderScopeOverride`, since such a scope cannot know about the overrides. This is one more reason to place the `ProviderScopeOverride` as the root widget of your test.
+
diff --git a/docs/src/content/docs/examples/auto-route.mdx b/docs/src/content/docs/examples/auto-route.mdx
index e279cacc..f0e4d453 100644
--- a/docs/src/content/docs/examples/auto-route.mdx
+++ b/docs/src/content/docs/examples/auto-route.mdx
@@ -136,7 +136,7 @@ class BooksWrapperPage extends StatelessWidget implements AutoRouteWrapper {
Widget wrappedRoute(BuildContext context) {
// Provide the books controller to descendants
return ProviderScope(
- providers: [booksControllerProvider],
+ providers: [booksControllerProvider()],
child: this,
);
}
diff --git a/docs/src/content/docs/examples/basic.mdx b/docs/src/content/docs/examples/basic.mdx
index 78db39ca..acbe6e60 100644
--- a/docs/src/content/docs/examples/basic.mdx
+++ b/docs/src/content/docs/examples/basic.mdx
@@ -60,7 +60,7 @@ class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
// Provide the modelProvider to descendants
return ProviderScope(
- providers: [modelProvider],
+ providers: [modelProvider()],
// This builder gives a descendant context, only descendants can access
// this scope
child: Builder(
@@ -146,7 +146,7 @@ void main() {
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- modelProvider.overrideWithValue(MockModel()),
+ modelProvider.overrideWith(Provider((context) => MockModel())),
],
child: const MainApp(),
),
diff --git a/docs/src/content/docs/examples/bloc.mdx b/docs/src/content/docs/examples/bloc.mdx
index 4ec4e97a..84a1c842 100644
--- a/docs/src/content/docs/examples/bloc.mdx
+++ b/docs/src/content/docs/examples/bloc.mdx
@@ -147,7 +147,7 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
- providers: [themeProvider],
+ providers: [themeProvider()],
child: Builder(
builder: (context) {
return BlocBuilder(
diff --git a/docs/src/content/docs/examples/solidart.mdx b/docs/src/content/docs/examples/solidart.mdx
index 8812e024..fff0d380 100644
--- a/docs/src/content/docs/examples/solidart.mdx
+++ b/docs/src/content/docs/examples/solidart.mdx
@@ -191,7 +191,7 @@ class TodosPage extends StatelessWidget {
Widget build(BuildContext context) {
// Using ProviderScope here to provide the [TodosController] to descendants.
return ProviderScope(
- providers: [todosControllerProvider],
+ providers: [todosControllerProvider()],
child: Scaffold(
appBar: AppBar(
title: const Text('Todos'),
@@ -247,7 +247,7 @@ class _TodosBodyState extends State {
providers: [
// make the active filter signal visible only to descendants.
// scoped here because this is where it starts to be necessary.
- todosFilterProvider,
+ todosFilterProvider(),
],
child: Column(
children: [
@@ -577,7 +577,9 @@ Widget wrapWithMockedTodosController({
return MaterialApp(
home: ProviderScopeOverride(
overrides: [
- todosControllerProvider.overrideWithValue(todosController),
+ todosControllerProvider.overrideWith(
+ Provider((context) => todosController),
+ ),
],
child: child,
),
diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx
index a9e4bcf3..601d67a8 100644
--- a/docs/src/content/docs/index.mdx
+++ b/docs/src/content/docs/index.mdx
@@ -23,7 +23,7 @@ The package supports many features, like providers that accept arguments. But to
```dart
ProviderScope(
- providers: [modelProvider],
+ providers: [modelProvider()],
child: MyWidget(),
)
```
diff --git a/docs/src/content/docs/installing.md b/docs/src/content/docs/installing.md
index 5a1be718..3c2c6948 100644
--- a/docs/src/content/docs/installing.md
+++ b/docs/src/content/docs/installing.md
@@ -21,11 +21,11 @@ Alternatively, you can add Disco manually by updating your `pubspec.yaml` file a
name: # your app name
environment:
- sdk: ^3.10.0 # Dart SDK version must be >=3.6.0 to support disco and >=3.10.0 to support disco_lint
- flutter: ">=3.27.0"
+ sdk: ^3.10.0 # Dart SDK version must be >=3.10.0 to support disco and >=3.10.3 to support disco_lint
+ flutter: ">=3.38.0"
dependencies:
- disco: ^1.0.0
+ disco: ^3.0.0
flutter:
sdk: flutter
```
@@ -35,7 +35,7 @@ After updating the file, run `flutter pub get` in your terminal to fetch the dep
## Linter
Disco provides an analyzer package called `disco_lint` to help you avoid common mistakes and simplify repetitive tasks (e.g. `Wrap with ProviderScope`).
-Be sure to have the Dart SDK version `>= 3.10.0` and the Flutter SDK `>= 3.38.0`.
+Be sure to have the Dart SDK version `>= 3.10.3` and the Flutter SDK `>= 3.38.0`.
Then edit your `analysis_options.yaml` file and add these lines of code:
diff --git a/docs/src/content/docs/miscellaneous/comparison-with-alternatives.mdx b/docs/src/content/docs/miscellaneous/comparison-with-alternatives.mdx
index 37225dba..6740d909 100644
--- a/docs/src/content/docs/miscellaneous/comparison-with-alternatives.mdx
+++ b/docs/src/content/docs/miscellaneous/comparison-with-alternatives.mdx
@@ -114,7 +114,7 @@ You insert a `ProviderScope` **where** you want the providers to be active — n
```dart
ProviderScope(
- providers: [modelProvider, secondModelProvider],
+ providers: [modelProvider(), secondModelProvider()],
child: MyWidget(),
)
```
diff --git a/docs/src/content/docs/miscellaneous/reactivity.md b/docs/src/content/docs/miscellaneous/reactivity.md
index 1cdb4235..2aa16215 100644
--- a/docs/src/content/docs/miscellaneous/reactivity.md
+++ b/docs/src/content/docs/miscellaneous/reactivity.md
@@ -21,7 +21,7 @@ runApp(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [counterProvider, doubleCounterProvider],
+ providers: [counterProvider(), doubleCounterProvider()],
child: SignalBuilder(
builder: (context, child) {
final counter = counterProvider.of(context);
diff --git a/examples/auto_route/lib/pages/books.dart b/examples/auto_route/lib/pages/books.dart
index fae696d3..eeb7d6f6 100644
--- a/examples/auto_route/lib/pages/books.dart
+++ b/examples/auto_route/lib/pages/books.dart
@@ -10,7 +10,7 @@ class BooksWrapperPage extends StatelessWidget implements AutoRouteWrapper {
@override
Widget wrappedRoute(BuildContext context) {
return ProviderScope(
- providers: [booksControllerProvider],
+ providers: [booksControllerProvider()],
child: this,
);
}
diff --git a/examples/bloc/lib/main.dart b/examples/bloc/lib/main.dart
index 0f92f906..a0bf8630 100644
--- a/examples/bloc/lib/main.dart
+++ b/examples/bloc/lib/main.dart
@@ -14,7 +14,7 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
- providers: [themeProvider],
+ providers: [themeProvider()],
child: Builder(
builder: (context) {
return BlocBuilder(
diff --git a/examples/solidart/lib/pages/todos.dart b/examples/solidart/lib/pages/todos.dart
index 05af2e2c..55dde055 100644
--- a/examples/solidart/lib/pages/todos.dart
+++ b/examples/solidart/lib/pages/todos.dart
@@ -10,7 +10,7 @@ class TodosPage extends StatelessWidget {
Widget build(BuildContext context) {
// Using ProviderScope here to provide the [TodosController] to descendants.
return ProviderScope(
- providers: [todosControllerProvider],
+ providers: [todosControllerProvider()],
child: Scaffold(
appBar: AppBar(
title: const Text('Todos'),
diff --git a/examples/solidart/lib/widgets/todos_body.dart b/examples/solidart/lib/widgets/todos_body.dart
index e45519cb..4cdb7a16 100644
--- a/examples/solidart/lib/widgets/todos_body.dart
+++ b/examples/solidart/lib/widgets/todos_body.dart
@@ -34,7 +34,7 @@ class _TodosBodyState extends State {
providers: [
// make the active filter signal visible only to descendants.
// scoped here because this is where it starts to be necessary.
- todosFilterProvider,
+ todosFilterProvider(),
],
child: Column(
children: [
diff --git a/examples/solidart/test/widget_test.dart b/examples/solidart/test/widget_test.dart
index 5a9d8999..e93461ba 100644
--- a/examples/solidart/test/widget_test.dart
+++ b/examples/solidart/test/widget_test.dart
@@ -21,7 +21,9 @@ Widget wrapWithMockedTodosController({
return MaterialApp(
home: ProviderScopeOverride(
overrides: [
- todosControllerProvider.overrideWithValue(todosController),
+ todosControllerProvider.overrideWith(
+ Provider((context) => todosController),
+ ),
],
child: child,
),
diff --git a/packages/disco/.gitignore b/packages/disco/.gitignore
new file mode 100644
index 00000000..3820a95c
--- /dev/null
+++ b/packages/disco/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/disco/.metadata b/packages/disco/.metadata
new file mode 100644
index 00000000..6091d697
--- /dev/null
+++ b/packages/disco/.metadata
@@ -0,0 +1,20 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa"
+ channel: "stable"
+
+project_type: package
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
+ base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
+ - platform: linux
+ create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
+ base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
diff --git a/packages/disco/CHANGELOG.md b/packages/disco/CHANGELOG.md
index 68d8b212..38b6905e 100644
--- a/packages/disco/CHANGELOG.md
+++ b/packages/disco/CHANGELOG.md
@@ -1,3 +1,36 @@
+## 3.0.0
+
+- **BREAKING**: A provider has to be called to be inserted into a `ProviderScope`, i.e. `providers: [myProvider()]` instead of `providers: [myProvider]`. This makes the syntax the same for both providers and argument providers.
+- **BREAKING**: `provider.overrideWithValue(value)` is deprecated in favor of `provider.overrideWith(provider)`, which overrides a provider entirely (and not just its value). The mock is a regular provider, with its own `create` and `dispose`. Argument providers are overridden with argument providers, which receive the argument specified in the widget tree. Migrate by replacing `provider.overrideWithValue(value)` with `provider.overrideWith(Provider((_) => value))`.
+- **BREAKING**: The values of the providers are now always created lazily: the `lazy` parameter and `DiscoConfig` (whose only option was `lazy`) have been removed. To create a value as soon as its scope is mounted, inject it in a widget placed below the scope:
+
+ ```dart
+ ProviderScope(
+ providers: [myProvider()],
+ child: Builder(
+ builder: (context) {
+ myProvider.of(context);
+ return const MyChild();
+ },
+ ),
+ )
+ ```
+
+ See [Lazy creation of the values](https://disco.mariuti.com/core/providers/#lazy-creation-of-the-values) for the details.
+
+- **BREAKING**: The order in which the providers of a `ProviderScope` are declared no longer matters, therefore `ProviderForwardReferenceError` has been removed. Only actual cycles are rejected, with the new `ProviderCircularDependencyError`. See [Scoping correctly with context](https://disco.mariuti.com/core/scoped-di/#scoping-correctly-with-context).
+- **FEAT**: The value of an overridden provider now lives in the very same `ProviderScope` where the value of the original provider would have lived, i.e. a mock has the exact same lifecycle as the provider it replaces. See [Testing](https://disco.mariuti.com/core/testing/).
+- **FEAT**: The values of a `ProviderScope` are disposed in the reverse order of their creation, so that a value is always disposed before the values it depends on. A throwing `dispose` no longer prevents the remaining values from being disposed.
+- **FEAT**: Inserting a provider into (or removing one from) the `providers` list of a mounted `ProviderScope` used to be silently ignored; it is now reported as an error in debug mode. See [Immutability](https://disco.mariuti.com/core/immutability/#the-set-of-providers-is-fixed).
+- **FIX**: A provider injecting an overridden provider of the same scope got the original provider instead of its override.
+- **FIX**: The value of an overridden provider is no longer created (it used to be created, and disposed, whenever the original provider was not lazy).
+- **FIX**: A mock passed to `overrideWith` can now inject other providers.
+- **FIX**: Reusing the same mock to override more than one provider no longer makes those providers share a single value.
+- **CHORE**: `ProviderScope.providers` and `ProviderScope.overrides` are no longer part of the public API, as exactly one of them was always null.
+- **CHORE**: The bookkeeping of `ProviderScopeState` (its maps of providers and values, and its lookup and creation methods) and `ProviderScopeOverrideState.providerScopeState` are now library-private.
+- **CHORE**: The bookkeeping of `ProviderScopeState` (its maps of providers and values, and its lookup and creation methods) and `ProviderScopeOverrideState.providerScopeState` are now library-private.
+- **CHORE**: Rename `InstantiableProvider` to `ValueBinding`.
+
## 2.0.0
- **FEAT**: Allow providers in the same `ProviderScope` to depend on previously declared providers. This simplifies the development experience. This friendlier syntax does not introduce circular dependencies.
diff --git a/packages/disco/README.md b/packages/disco/README.md
index d54800e1..9f70ed03 100644
--- a/packages/disco/README.md
+++ b/packages/disco/README.md
@@ -41,11 +41,13 @@ The package supports many features, like providers that accept arguments. But to
```dart
ProviderScope(
- providers: [modelProvider],
+ providers: [modelProvider()],
child: MyWidget(),
)
```
+ **Note:** calling the provider (i.e. `modelProvider()`) does not create anything by itself; it is just how a provider is inserted into a scope, and it is also what allows an argument to be passed to providers created with `Provider.withArgument`.
+
**Note:** the actual state for the provider is created and stored inside the `ProviderScope` instance where the provider is referenced.
This way, when the ProviderScope gets disposed, the state gets disposed — making it ideal for managing **local state**.
diff --git a/packages/disco/benchmark/provider_benchmark.dart b/packages/disco/benchmark/provider_benchmark.dart
index a4e813f6..e86e42da 100644
--- a/packages/disco/benchmark/provider_benchmark.dart
+++ b/packages/disco/benchmark/provider_benchmark.dart
@@ -7,29 +7,55 @@ import 'package:flutter_test/flutter_test.dart';
/// Comprehensive benchmark suite for provider performance testing.
///
+/// The value of a provider is always created lazily, i.e. the first time it is
+/// injected. Therefore the benchmarks distinguish between:
+/// - registering the providers, which is what mounting a ProviderScope does;
+/// - creating their values, which is what injecting them does.
+///
/// This benchmark tests various scenarios:
-/// - Creating N simple providers (lazy and eager)
-/// - Creating N providers with dependencies
-/// - Retrieving provider values
+/// - Registering N providers
+/// - Creating the values of N providers
+/// - Creating N values with dependencies
/// - ArgProviders performance
/// - Nested scope performance
// Global map to store benchmark results
final Map _benchmarkResults = {};
+/// Instantiates all the [providers], so that they can be inserted into a
+/// [ProviderScope].
+List _instantiateAll(
+ Iterable> providers,
+) => [for (final provider in providers) provider()];
+
+/// A widget which injects all the [providers], so that their values are
+/// created.
+class _InjectAll extends StatelessWidget {
+ const _InjectAll(this.providers);
+
+ final Iterable> providers;
+
+ @override
+ Widget build(BuildContext context) {
+ for (final provider in providers) {
+ provider.of(context);
+ }
+ return Container();
+ }
+}
+
void main() {
// Write results to file after all tests complete
tearDownAll(_writeBenchmarkResults);
group('Provider Benchmark', () {
- testWidgets('Benchmark: Create 100 simple eager providers', (tester) async {
+ testWidgets('Benchmark: Register 100 providers', (tester) async {
final stopwatch = Stopwatch()..start();
final providers = List.generate(
100,
(i) => Provider(
(_) => 'Value$i',
- lazy: false,
debugName: 'provider$i',
),
);
@@ -37,7 +63,8 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
+ providers: _instantiateAll(providers),
+ // No value is created, since nothing is injected.
child: Container(),
),
),
@@ -45,18 +72,17 @@ void main() {
stopwatch.stop();
final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Create 100 simple eager providers'] = time;
- print('Create 100 simple eager providers: ${time}ms');
+ _benchmarkResults['Register 100 providers'] = time;
+ print('Register 100 providers: ${time}ms');
});
- testWidgets('Benchmark: Create 100 simple lazy providers', (tester) async {
+ testWidgets('Benchmark: Create 100 provider values', (tester) async {
final stopwatch = Stopwatch()..start();
final providers = List.generate(
100,
(i) => Provider(
(_) => 'Value$i',
- lazy: true,
debugName: 'provider$i',
),
);
@@ -64,29 +90,28 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: _instantiateAll(providers),
+ child: _InjectAll(providers),
),
),
);
stopwatch.stop();
final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Create 100 simple lazy providers'] = time;
- print('Create 100 simple lazy providers: ${time}ms');
+ _benchmarkResults['Create 100 provider values'] = time;
+ print('Create 100 provider values: ${time}ms');
});
- testWidgets('Benchmark: Create 50 providers with dependencies', (
+ testWidgets('Benchmark: Create 50 values with dependencies', (
tester,
) async {
// Create a chain of providers where each depends on the previous one
- final providers = [];
+ final providers = >[];
// First provider has no dependencies
providers.add(
Provider(
(_) => 0,
- lazy: false,
debugName: 'provider0',
),
);
@@ -96,38 +121,39 @@ void main() {
providers.add(
Provider(
(context) {
- final prev = providers[i - 1].of(context) as int;
+ final prev = providers[i - 1].of(context);
return prev + 1;
},
- lazy: false,
debugName: 'provider$i',
),
);
}
+ final instantiatedProviders = _instantiateAll(providers);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ // Injecting the last provider creates the whole chain.
+ child: _InjectAll([providers.last]),
),
),
);
stopwatch.stop();
final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Create 50 providers with dependencies'] = time;
- print('Create 50 providers with dependencies: ${time}ms');
+ _benchmarkResults['Create 50 values with dependencies'] = time;
+ print('Create 50 values with dependencies: ${time}ms');
});
- testWidgets('Benchmark: Retrieve 100 lazy provider values', (tester) async {
+ testWidgets('Benchmark: Retrieve 100 provider values', (tester) async {
final providers = List.generate(
100,
(i) => Provider(
(_) => 'Value$i',
- lazy: true,
debugName: 'provider$i',
),
);
@@ -135,20 +161,20 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
+ providers: _instantiateAll(providers),
child: Builder(
builder: (context) {
final stopwatch = Stopwatch()..start();
- // Access all lazy providers to trigger creation
+ // Access all providers to trigger creation
for (final provider in providers) {
provider.of(context);
}
stopwatch.stop();
final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Retrieve 100 lazy provider values'] = time;
- print('Retrieve 100 lazy provider values: ${time}ms');
+ _benchmarkResults['Retrieve 100 provider values'] = time;
+ print('Retrieve 100 provider values: ${time}ms');
return Container();
},
@@ -158,12 +184,11 @@ void main() {
);
});
- testWidgets('Benchmark: Create 100 ArgProviders', (tester) async {
+ testWidgets('Benchmark: Create 100 ArgProvider values', (tester) async {
final argProviders = List.generate(
100,
(i) => Provider.withArgument(
(_, arg) => 'Value$i-$arg',
- lazy: false,
debugName: 'argProvider$i',
),
);
@@ -176,15 +201,22 @@ void main() {
MaterialApp(
home: ProviderScope(
providers: instantiated,
- child: Container(),
+ child: Builder(
+ builder: (context) {
+ for (final argProvider in argProviders) {
+ argProvider.of(context);
+ }
+ return Container();
+ },
+ ),
),
),
);
stopwatch.stop();
final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Create 100 ArgProviders'] = time;
- print('Create 100 ArgProviders: ${time}ms');
+ _benchmarkResults['Create 100 ArgProvider values'] = time;
+ print('Create 100 ArgProvider values: ${time}ms');
});
testWidgets('Benchmark: Access providers in nested scopes', (tester) async {
@@ -192,7 +224,6 @@ void main() {
50,
(i) => Provider(
(_) => 'Outer$i',
- lazy: false,
debugName: 'outerProvider$i',
),
);
@@ -201,19 +232,21 @@ void main() {
50,
(i) => Provider(
(_) => 'Inner$i',
- lazy: false,
debugName: 'innerProvider$i',
),
);
+ final instantiatedOuterProviders = _instantiateAll(outerProviders);
+ final instantiatedInnerProviders = _instantiateAll(innerProviders);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: outerProviders,
+ providers: instantiatedOuterProviders,
child: ProviderScope(
- providers: innerProviders,
+ providers: instantiatedInnerProviders,
child: Builder(
builder: (context) {
// Access outer providers from inner scope
@@ -241,7 +274,7 @@ void main() {
testWidgets('Benchmark: Complex dependency chain with 30 providers', (
tester,
) async {
- final providers = [];
+ final providers = >[];
// Create a more complex dependency pattern
// Base providers (0-9)
@@ -249,7 +282,6 @@ void main() {
providers.add(
Provider(
(_) => i,
- lazy: false,
debugName: 'base$i',
),
);
@@ -260,11 +292,10 @@ void main() {
providers.add(
Provider(
(context) {
- final base1 = providers[i - 10].of(context) as int;
- final base2 = providers[i - 9].of(context) as int;
+ final base1 = providers[i - 10].of(context);
+ final base2 = providers[i - 9].of(context);
return base1 + base2;
},
- lazy: false,
debugName: 'mid$i',
),
);
@@ -275,23 +306,26 @@ void main() {
providers.add(
Provider(
(context) {
- final mid1 = providers[i - 10].of(context) as int;
- final mid2 = providers[i - 9].of(context) as int;
+ final mid1 = providers[i - 10].of(context);
+ final mid2 = providers[i - 9].of(context);
return mid1 + mid2;
},
- lazy: false,
debugName: 'top$i',
),
);
}
+ final instantiatedProviders = _instantiateAll(providers);
+ // Injecting the top-level providers creates all the others.
+ final topProviders = providers.sublist(20);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ child: _InjectAll(topProviders),
),
),
);
@@ -302,60 +336,16 @@ void main() {
print('Complex dependency chain with 30 providers: ${time}ms');
});
- testWidgets('Benchmark: Mixed lazy and eager providers (100 total)', (
- tester,
- ) async {
- final providers = [];
-
- // 50 eager providers
- for (var i = 0; i < 50; i++) {
- providers.add(
- Provider(
- (_) => 'Eager$i',
- lazy: false,
- debugName: 'eager$i',
- ),
- );
- }
-
- // 50 lazy providers
- for (var i = 50; i < 100; i++) {
- providers.add(
- Provider(
- (_) => 'Lazy$i',
- lazy: true,
- debugName: 'lazy$i',
- ),
- );
- }
-
- final stopwatch = Stopwatch()..start();
-
- await tester.pumpWidget(
- MaterialApp(
- home: ProviderScope(
- providers: providers,
- child: Container(),
- ),
- ),
- );
-
- stopwatch.stop();
- final time = stopwatch.elapsedMilliseconds;
- _benchmarkResults['Mixed lazy and eager (100 total)'] = time;
- print('Mixed lazy and eager providers (100 total): ${time}ms');
- });
-
testWidgets('Benchmark: ArgProviders with dependencies', (tester) async {
- final providers = [];
+ final providers = [];
+ final argProviders = >[];
// Base provider
final baseProvider = Provider(
(_) => 10,
- lazy: false,
debugName: 'base',
);
- providers.add(baseProvider);
+ providers.add(baseProvider());
// ArgProviders that depend on base
for (var i = 0; i < 50; i++) {
@@ -364,9 +354,9 @@ void main() {
final base = baseProvider.of(context);
return base + arg + i;
},
- lazy: false,
debugName: 'argProvider$i',
);
+ argProviders.add(argProvider);
providers.add(argProvider.call(i));
}
@@ -376,7 +366,14 @@ void main() {
MaterialApp(
home: ProviderScope(
providers: providers,
- child: Container(),
+ child: Builder(
+ builder: (context) {
+ for (final argProvider in argProviders) {
+ argProvider.of(context);
+ }
+ return Container();
+ },
+ ),
),
),
);
@@ -392,18 +389,19 @@ void main() {
500,
(i) => Provider(
(_) => 'Value$i',
- lazy: i.isOdd, // Alternate between lazy and eager
debugName: 'provider$i',
),
);
+ final instantiatedProviders = _instantiateAll(providers);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ child: _InjectAll(providers),
),
),
);
@@ -417,12 +415,11 @@ void main() {
group('Provider Benchmark - Stress Tests', () {
testWidgets('Stress: Deep dependency chain (100 levels)', (tester) async {
- final providers = [];
+ final providers = >[];
providers.add(
Provider(
(_) => 0,
- lazy: false,
debugName: 'provider0',
),
);
@@ -431,22 +428,23 @@ void main() {
providers.add(
Provider(
(context) {
- final prev = providers[i - 1].of(context) as int;
+ final prev = providers[i - 1].of(context);
return prev + 1;
},
- lazy: false,
debugName: 'provider$i',
),
);
}
+ final instantiatedProviders = _instantiateAll(providers);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ child: _InjectAll([providers.last]),
),
),
);
@@ -460,10 +458,10 @@ void main() {
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
+ providers: instantiatedProviders,
child: Builder(
builder: (context) {
- final lastValue = providers.last.of(context) as int;
+ final lastValue = providers.last.of(context);
expect(lastValue, 99);
return Container();
},
@@ -476,11 +474,10 @@ void main() {
testWidgets('Stress: Wide dependency tree (base + 100 dependents)', (
tester,
) async {
- final providers = [];
+ final providers = >[];
final baseProvider = Provider(
(_) => 42,
- lazy: false,
debugName: 'base',
);
providers.add(baseProvider);
@@ -492,19 +489,20 @@ void main() {
final base = baseProvider.of(context);
return base + i;
},
- lazy: false,
debugName: 'dependent$i',
),
);
}
+ final instantiatedProviders = _instantiateAll(providers);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ child: _InjectAll(providers),
),
),
);
@@ -520,26 +518,27 @@ void main() {
20,
(i) => Provider(
(_) => 'Value$i',
- lazy: false,
debugName: 'provider$i',
),
);
+ final instantiatedProviders = _instantiateAll(providers);
+
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(
MaterialApp(
home: ProviderScope(
- providers: providers,
+ providers: instantiatedProviders,
child: ProviderScope(
- providers: providers,
+ providers: instantiatedProviders,
child: ProviderScope(
- providers: providers,
+ providers: instantiatedProviders,
child: ProviderScope(
- providers: providers,
+ providers: instantiatedProviders,
child: ProviderScope(
- providers: providers,
- child: Container(),
+ providers: instantiatedProviders,
+ child: _InjectAll(providers),
),
),
),
@@ -573,14 +572,13 @@ void _writeBenchmarkResults() {
// Write results in the expected order
final orderedKeys = [
- 'Create 100 simple eager providers',
- 'Create 100 simple lazy providers',
- 'Create 50 providers with dependencies',
- 'Retrieve 100 lazy provider values',
- 'Create 100 ArgProviders',
+ 'Register 100 providers',
+ 'Create 100 provider values',
+ 'Create 50 values with dependencies',
+ 'Retrieve 100 provider values',
+ 'Create 100 ArgProvider values',
'Access 100 providers in nested scopes',
'Complex dependency chain (30 providers)',
- 'Mixed lazy and eager (100 total)',
'ArgProviders with dependencies (50)',
'Large scale (500 providers)',
'Deep dependency chain (100 levels)',
diff --git a/packages/disco/benchmark_results.md b/packages/disco/benchmark_results.md
new file mode 100644
index 00000000..bc65a805
--- /dev/null
+++ b/packages/disco/benchmark_results.md
@@ -0,0 +1,20 @@
+# Provider Benchmark Results
+
+**Date**: 2026-08-08 19:46:29 UTC
+
+## Results
+
+| Benchmark | Time (ms) |
+|-----------|-----------|
+| Register 100 providers | 163 |
+| Create 100 provider values | 19 |
+| Create 50 values with dependencies | 16 |
+| Retrieve 100 provider values | 0 |
+| Create 100 ArgProvider values | 18 |
+| Access 100 providers in nested scopes | 8 |
+| Complex dependency chain (30 providers) | 18 |
+| ArgProviders with dependencies (50) | 19 |
+| Large scale (500 providers) | 7 |
+| Deep dependency chain (100 levels) | 18 |
+| Wide dependency tree (100 dependents) | 20 |
+| Multiple nested scopes (5 levels) | 20 |
diff --git a/packages/disco/example/README.md b/packages/disco/example/README.md
index d5f7eb26..7a795945 100644
--- a/packages/disco/example/README.md
+++ b/packages/disco/example/README.md
@@ -1,3 +1,30 @@
# disco (example)
-An example that uses Disco.
+An example that uses Disco. Every page demonstrates a different feature, and a
+"lifecycle log" panel shows when the values of the providers get created and
+disposed.
+
+| Page | Shows |
+| --- | --- |
+| Home | A plain provider scoped to the page, injected with `of(context)` |
+| Providers with arguments | `Provider.withArgument`, `dispose`, and injecting a provider of an ancestor scope |
+| Nested scopes | Two scopes providing the same provider: the nearest one wins |
+| Modals | `ProviderScopePortal`, and what happens in a dialog without it |
+| Lazy creation | When the values get created, and how to create one as soon as a scope is mounted |
+| Recreating a scope | Forcing a new value by changing the `key` of a `ProviderScope` |
+| Missing providers | `of` throwing a `ProviderWithoutScopeError`, versus `maybeOf` returning `null` |
+
+Overriding providers with `overrideWith` is meant for testing only, therefore
+it is demonstrated in `test/disco_test.dart` instead.
+
+## Run
+
+```sh
+flutter run
+```
+
+## Test
+
+```sh
+flutter test
+```
diff --git a/packages/disco/example/analysis_options.yaml b/packages/disco/example/analysis_options.yaml
index 66f6445a..9673fccb 100644
--- a/packages/disco/example/analysis_options.yaml
+++ b/packages/disco/example/analysis_options.yaml
@@ -1,3 +1,6 @@
+analyzer:
+ errors:
+ specify_nonobvious_property_types: ignore
include: package:very_good_analysis/analysis_options.yaml
linter:
diff --git a/packages/disco/example/lib/main.dart b/packages/disco/example/lib/main.dart
index 087a5858..bc0ae3b2 100644
--- a/packages/disco/example/lib/main.dart
+++ b/packages/disco/example/lib/main.dart
@@ -1,6 +1,47 @@
+// The example catches an Error on purpose, in order to show the message of a
+// ProviderWithoutScopeError in the UI (see ErrorsPage).
+// ignore_for_file: avoid_catching_errors
+
+import 'dart:async';
+
import 'package:disco/disco.dart';
import 'package:flutter/material.dart';
+// ---------------------------------------------------------------------------
+// Models
+// ---------------------------------------------------------------------------
+
+/// Collects the messages logged while the values of the providers get created
+/// and disposed, so that their lifecycle is visible in the UI.
+class Logger extends ChangeNotifier {
+ final List _messages = [];
+ bool _disposed = false;
+
+ List get messages => List.unmodifiable(_messages);
+
+ void log(String message) {
+ _messages.add(message);
+ // The value of a lazy provider is created while the widget tree is being
+ // built, and disposed while it is being unmounted. Notifying the listeners
+ // right away would rebuild widgets during a build, which is not allowed by
+ // Flutter. Therefore, the notification is postponed.
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!_disposed) notifyListeners();
+ });
+ }
+
+ void clear() {
+ _messages.clear();
+ notifyListeners();
+ }
+
+ @override
+ void dispose() {
+ _disposed = true;
+ super.dispose();
+ }
+}
+
abstract class Model extends ChangeNotifier {
void incrementCounter();
@@ -20,7 +61,122 @@ class ModelImplementation extends Model {
}
}
-final modelProvider = Provider((context) => ModelImplementation());
+/// A value built out of an argument (see [userProvider]).
+class User {
+ const User({required this.id, required this.name});
+
+ final String id;
+ final String name;
+}
+
+/// A value which needs an argument, another provider and a disposal
+/// (see [cartProvider]).
+class Cart extends ChangeNotifier {
+ Cart({required Logger logger, required int itemCount})
+ : _logger = logger,
+ _itemCount = itemCount;
+
+ final Logger _logger;
+ int _itemCount;
+
+ int get itemCount => _itemCount;
+
+ void addItem() {
+ _itemCount++;
+ _logger.log('Cart: item added, $_itemCount in total');
+ notifyListeners();
+ }
+
+ @override
+ void dispose() {
+ _logger.log('Cart disposed');
+ super.dispose();
+ }
+}
+
+/// A value which depends on another provider of the same scope
+/// (see [analyticsProvider]).
+class Analytics {
+ const Analytics(this._logger);
+
+ final Logger _logger;
+
+ void track(String event) => _logger.log('Analytics: $event');
+}
+
+// ---------------------------------------------------------------------------
+// Providers
+//
+// Providers are declared at the top level, but they do not hold any state:
+// they are only used as type-safe identifiers. The values are created and
+// stored by the ProviderScope which provides them.
+// ---------------------------------------------------------------------------
+
+/// A plain provider. Its value is created lazily, i.e. the first time it gets
+/// injected.
+final modelProvider = Provider(
+ (context) => ModelImplementation(),
+ debugName: 'model',
+);
+
+/// A provider with a `dispose` callback: its value is disposed when the
+/// ProviderScope providing it is unmounted.
+///
+/// See [MainApp] for how its value is created as soon as the app starts.
+final loggerProvider = Provider(
+ (context) => Logger()..log('Logger created'),
+ dispose: (logger) => logger.dispose(),
+ debugName: 'logger',
+);
+
+/// A provider depending on another provider of the **same** scope.
+final analyticsProvider = Provider(
+ (context) {
+ final logger = loggerProvider.of(context)..log('Analytics created lazily');
+ return Analytics(logger);
+ },
+ debugName: 'analytics',
+);
+
+/// A provider which needs an argument.
+final userProvider = Provider.withArgument(
+ (context, String id) {
+ loggerProvider.of(context).log('User $id created');
+ return User(id: id, name: 'User $id');
+ },
+ debugName: 'user',
+);
+
+/// An argument provider which injects a provider of an **ancestor** scope and
+/// disposes its value.
+final cartProvider = Provider.withArgument(
+ (context, int initialItemCount) {
+ final logger = loggerProvider.of(context)
+ ..log('Cart created with $initialItemCount item(s)');
+ return Cart(logger: logger, itemCount: initialItemCount);
+ },
+ dispose: (cart) => cart.dispose(),
+ debugName: 'cart',
+);
+
+/// Provided by two nested scopes with different arguments, to show that the
+/// nearest scope wins.
+// ignore: specify_nonobvious_property_types
+final labelProvider = Provider.withArgument(
+ (context, String label) => label,
+ debugName: 'label',
+);
+
+/// Deliberately never inserted into any ProviderScope, to show the difference
+/// between `of` and `maybeOf`.
+final missingProvider = Provider(
+ (context) => 'unreachable',
+ debugName: 'missing',
+);
+
+// ---------------------------------------------------------------------------
+// App
+// ---------------------------------------------------------------------------
void main() {
runApp(const MainApp());
@@ -31,24 +187,43 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Disco Example',
- theme: ThemeData(
- primarySwatch: Colors.blue,
+ // Application-wide scope: the logger, and the analytics depending on it,
+ // live as long as the app does.
+ return ProviderScope(
+ providers: [
+ loggerProvider(),
+ analyticsProvider(),
+ ],
+ // The values of the providers are always created lazily. Injecting the
+ // logger right below the scope creates it as soon as the app starts:
+ // this is the recommended way of initializing a value eagerly.
+ child: Builder(
+ builder: (context) {
+ loggerProvider.of(context);
+ return MaterialApp(
+ title: 'Disco Example',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: const HomePage(),
+ );
+ },
),
- home: const MyHomePage(),
);
}
}
-class MyHomePage extends StatelessWidget {
- const MyHomePage({super.key});
+/// Shows a counter provided by a page-scoped provider, plus a list of the
+/// other demos.
+class HomePage extends StatelessWidget {
+ const HomePage({super.key});
@override
Widget build(BuildContext context) {
- // Provide the modelProvider to descendants
+ // The counter is scoped to this page: it is created when the page is
+ // shown and disposed when the page is removed.
return ProviderScope(
- providers: [modelProvider],
+ providers: [modelProvider()],
// This builder gives a descendant context, only descendants can access
// this scope
child: Builder(
@@ -56,22 +231,61 @@ class MyHomePage extends StatelessWidget {
// retrieve the model
final model = modelProvider.of(context);
return Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Text(
- 'You have pushed the button this many times:',
+ appBar: AppBar(title: const Text('Disco example')),
+ body: Column(
+ children: [
+ const SizedBox(height: 16),
+ const Text(
+ 'You have pushed the button this many times:',
+ ),
+ // Rebuilds this widget when the model changes
+ ListenableBuilder(
+ listenable: model,
+ builder: (context, child) {
+ return Text(model.counter.toString());
+ },
+ ),
+ const SizedBox(height: 16),
+ const Divider(height: 1),
+ Expanded(
+ child: ListView(
+ children: [
+ _DemoTile(
+ title: 'Providers with arguments',
+ subtitle: 'Provider.withArgument and dispose',
+ pageBuilder: (context) => const ArgumentsPage(),
+ ),
+ _DemoTile(
+ title: 'Nested scopes',
+ subtitle: 'The nearest scope wins',
+ pageBuilder: (context) => const NestedScopesPage(),
+ ),
+ _DemoTile(
+ title: 'Modals',
+ subtitle: 'ProviderScopePortal',
+ pageBuilder: (context) => const ModalsPage(),
+ ),
+ _DemoTile(
+ title: 'Lazy creation',
+ subtitle: 'When the values get created',
+ pageBuilder: (context) => const LazinessPage(),
+ ),
+ _DemoTile(
+ title: 'Recreating a scope',
+ subtitle: 'Changing the key of a ProviderScope',
+ pageBuilder: (context) => const ScopeKeyPage(),
+ ),
+ _DemoTile(
+ title: 'Missing providers',
+ subtitle: 'of versus maybeOf',
+ pageBuilder: (context) => const ErrorsPage(),
+ ),
+ ],
),
- // Rebuilds this widget when the model changes
- ListenableBuilder(
- listenable: model,
- builder: (context, child) {
- return Text(model.counter.toString());
- },
- ),
- ],
- ),
+ ),
+ const Divider(height: 1),
+ const SizedBox(height: 140, child: LogPanel()),
+ ],
),
floatingActionButton: FloatingActionButton(
// increment the counter when the button is pressed
@@ -84,3 +298,439 @@ class MyHomePage extends StatelessWidget {
);
}
}
+
+class _DemoTile extends StatelessWidget {
+ const _DemoTile({
+ required this.title,
+ required this.subtitle,
+ required this.pageBuilder,
+ });
+
+ final String title;
+ final String subtitle;
+ final WidgetBuilder pageBuilder;
+
+ @override
+ Widget build(BuildContext context) {
+ return ListTile(
+ title: Text(title),
+ subtitle: Text(subtitle),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () {
+ // `analyticsProvider` is lazy: its value is created here, the very
+ // first time it gets injected.
+ analyticsProvider.of(context).track('opened "$title"');
+ unawaited(
+ Navigator.of(context).push(
+ MaterialPageRoute(builder: pageBuilder),
+ ),
+ );
+ },
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Demos
+// ---------------------------------------------------------------------------
+
+/// Shows two argument providers, one of which injects a provider of an
+/// ancestor scope and gets disposed together with this page.
+class ArgumentsPage extends StatelessWidget {
+ const ArgumentsPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return ProviderScope(
+ // The argument is given where the provider is inserted into the tree.
+ providers: [
+ userProvider('42'),
+ cartProvider(2),
+ ],
+ child: Builder(
+ builder: (context) {
+ final user = userProvider.of(context);
+ final cart = cartProvider.of(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('Providers with arguments'),
+ ),
+ body: Column(
+ children: [
+ ListTile(
+ title: const Text('Injected user'),
+ subtitle: Text('${user.name}, id ${user.id}'),
+ ),
+ ListenableBuilder(
+ listenable: cart,
+ builder: (context, child) {
+ return ListTile(
+ title: const Text('Items in the cart'),
+ subtitle: Text(cart.itemCount.toString()),
+ );
+ },
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: ElevatedButton(
+ onPressed: cart.addItem,
+ child: const Text('Add an item'),
+ ),
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'Go back: the cart is disposed together with this page, '
+ 'while the logger of the app-wide scope survives.',
+ ),
+ ),
+ const Divider(height: 1),
+ const Expanded(child: LogPanel()),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
+
+/// Shows that, when two scopes provide the same provider, the nearest one
+/// wins.
+class NestedScopesPage extends StatelessWidget {
+ const NestedScopesPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Nested scopes')),
+ body: ProviderScope(
+ providers: [labelProvider('outer scope')],
+ child: Column(
+ children: [
+ Builder(
+ builder: (context) {
+ return ListTile(
+ title: const Text('Injected between the two scopes'),
+ subtitle: Text(labelProvider.of(context)),
+ );
+ },
+ ),
+ ProviderScope(
+ providers: [labelProvider('inner scope')],
+ child: Builder(
+ builder: (context) {
+ return ListTile(
+ title: const Text('Injected below the inner scope'),
+ subtitle: Text(labelProvider.of(context)),
+ );
+ },
+ ),
+ ),
+ const Padding(
+ padding: EdgeInsets.all(16),
+ child: Text(
+ 'The injection walks up the widget tree and stops at the '
+ 'first ProviderScope providing the requested provider.',
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+/// Shows that a modal is spawned in a new widget tree, and how
+/// [ProviderScopePortal] gives it access to the providers of the main tree.
+class ModalsPage extends StatelessWidget {
+ const ModalsPage({super.key});
+
+ Future _showDialogWithPortal(BuildContext mainContext) {
+ return showDialog(
+ context: mainContext,
+ builder: (dialogContext) {
+ return ProviderScopePortal(
+ // The context of the main tree, i.e. a descendant of the scope
+ // providing `userProvider`.
+ mainContext: mainContext,
+ child: Builder(
+ builder: (context) {
+ final user = userProvider.of(context);
+ return _DemoDialog(
+ title: 'With ProviderScopePortal',
+ content: 'Injected: ${user.name}',
+ );
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ Future _showDialogWithoutPortal(BuildContext mainContext) {
+ return showDialog(
+ context: mainContext,
+ builder: (dialogContext) {
+ // `maybeOf` returns null: the scope of this page is not an ancestor of
+ // the dialog. Note that `of` would throw a ProviderWithoutScopeError.
+ final user = userProvider.maybeOf(dialogContext);
+ return _DemoDialog(
+ title: 'Without ProviderScopePortal',
+ content: 'Injected: ${user?.name ?? 'null'}',
+ );
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ProviderScope(
+ providers: [userProvider('7')],
+ child: Scaffold(
+ appBar: AppBar(title: const Text('Modals')),
+ body: Builder(
+ builder: (context) {
+ return Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(16),
+ child: Text(
+ 'A modal is spawned in a new widget tree: only the '
+ 'Navigator is a common ancestor. The providers of this '
+ 'page are therefore not reachable from a dialog, unless a '
+ 'ProviderScopePortal is used.',
+ ),
+ ),
+ ElevatedButton(
+ onPressed: () => _showDialogWithPortal(context),
+ child: const Text('Show a dialog with the portal'),
+ ),
+ const SizedBox(height: 8),
+ ElevatedButton(
+ onPressed: () => _showDialogWithoutPortal(context),
+ child: const Text('Show a dialog without the portal'),
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class _DemoDialog extends StatelessWidget {
+ const _DemoDialog({required this.title, required this.content});
+
+ final String title;
+ final String content;
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ title: Text(title),
+ content: Text(content),
+ actions: [
+ TextButton(
+ onPressed: Navigator.of(context).pop,
+ child: const Text('Close'),
+ ),
+ ],
+ );
+ }
+}
+
+/// Shows when the value of a provider gets created.
+class LazinessPage extends StatelessWidget {
+ const LazinessPage({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Lazy creation')),
+ body: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(16),
+ child: Text(
+ 'The value of a provider is created the first time it gets '
+ 'injected, and never before. The logger was created at startup '
+ 'only because MainApp injects it right below the app-wide '
+ 'scope. The analytics were created later, the first time a demo '
+ 'was opened from the list.',
+ ),
+ ),
+ Builder(
+ builder: (context) {
+ return ElevatedButton(
+ onPressed: () {
+ analyticsProvider.of(context).track('button pressed');
+ },
+ child: const Text('Inject the analytics again'),
+ );
+ },
+ ),
+ const Divider(height: 1),
+ const Expanded(child: LogPanel()),
+ ],
+ ),
+ );
+ }
+}
+
+/// Shows how to force the recreation of the value of a provider, by changing
+/// the key of the [ProviderScope] providing it.
+class ScopeKeyPage extends StatefulWidget {
+ const ScopeKeyPage({super.key});
+
+ @override
+ State createState() => _ScopeKeyPageState();
+}
+
+class _ScopeKeyPageState extends State {
+ int _initialItemCount = 1;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Recreating a scope')),
+ body: Column(
+ children: [
+ const Padding(
+ padding: EdgeInsets.all(16),
+ child: Text(
+ 'The value of a provider is created once per scope: passing a '
+ 'different argument alone changes nothing. Changing the key of '
+ 'the ProviderScope recreates the whole subtree, and therefore '
+ 'the value too. This is possible, but discouraged.',
+ ),
+ ),
+ ProviderScope(
+ key: ValueKey(_initialItemCount),
+ providers: [cartProvider(_initialItemCount)],
+ child: Builder(
+ builder: (context) {
+ final cart = cartProvider.of(context);
+ return ListenableBuilder(
+ listenable: cart,
+ builder: (context, child) {
+ return ListTile(
+ title: const Text('Items in the cart'),
+ subtitle: Text(cart.itemCount.toString()),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ setState(() => _initialItemCount++);
+ },
+ child: const Text('Recreate the scope with one more item'),
+ ),
+ const Divider(height: 1),
+ const Expanded(child: LogPanel()),
+ ],
+ ),
+ );
+ }
+}
+
+/// Shows the difference between `of` and `maybeOf` for a provider which is not
+/// provided by any scope.
+class ErrorsPage extends StatefulWidget {
+ const ErrorsPage({super.key});
+
+ @override
+ State createState() => _ErrorsPageState();
+}
+
+class _ErrorsPageState extends State {
+ String? _error;
+
+ void _injectMissingProvider() {
+ try {
+ missingProvider.of(context);
+ } on ProviderWithoutScopeError catch (error) {
+ setState(() => _error = error.toString());
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final maybeValue = missingProvider.maybeOf(context);
+ return Scaffold(
+ appBar: AppBar(title: const Text('Missing providers')),
+ body: Column(
+ children: [
+ ListTile(
+ title: const Text('missingProvider.maybeOf(context)'),
+ subtitle: Text(maybeValue ?? 'null'),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16),
+ child: ElevatedButton(
+ onPressed: _injectMissingProvider,
+ child: const Text('missingProvider.of(context)'),
+ ),
+ ),
+ if (_error != null)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Text(_error!),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+/// Displays the messages of the [Logger] of the app-wide scope.
+class LogPanel extends StatelessWidget {
+ const LogPanel({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ // The app-wide scope is an ancestor of every page, therefore the logger is
+ // reachable from anywhere.
+ final logger = loggerProvider.of(context);
+ return ListenableBuilder(
+ listenable: logger,
+ builder: (context, child) {
+ final messages = logger.messages;
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Row(
+ children: [
+ const Padding(
+ padding: EdgeInsets.only(left: 16),
+ child: Text('Lifecycle log'),
+ ),
+ const Spacer(),
+ TextButton(
+ onPressed: logger.clear,
+ child: const Text('Clear'),
+ ),
+ ],
+ ),
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ itemCount: messages.length,
+ itemBuilder: (context, index) {
+ return Text(
+ messages[index],
+ style: Theme.of(context).textTheme.bodySmall,
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+}
diff --git a/packages/disco/example/test/disco_test.dart b/packages/disco/example/test/disco_test.dart
index c9dc060f..ea9e6e1f 100644
--- a/packages/disco/example/test/disco_test.dart
+++ b/packages/disco/example/test/disco_test.dart
@@ -43,7 +43,7 @@ void main() {
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- modelProvider.overrideWithValue(MockModel()),
+ modelProvider.overrideWith(Provider((context) => MockModel())),
],
child: const MainApp(),
),
diff --git a/packages/disco/lib/src/disco_internal.dart b/packages/disco/lib/src/disco_internal.dart
index 659c91b5..543c613b 100644
--- a/packages/disco/lib/src/disco_internal.dart
+++ b/packages/disco/lib/src/disco_internal.dart
@@ -3,13 +3,10 @@ import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
-part 'models/overrides/override.dart';
-part 'models/overrides/provider_argument_override.dart';
-part 'models/overrides/provider_override.dart';
-part 'models/providers/instantiable_provider.dart';
-part 'models/providers/provider_argument.dart';
+part 'models/override.dart';
+part 'models/value_binding.dart';
+part 'models/providers/arg_provider.dart';
part 'models/providers/provider.dart';
-part 'utils/disco_config.dart';
part 'utils/extensions.dart';
part 'widgets/provider_scope.dart';
part 'widgets/provider_scope_override.dart';
diff --git a/packages/disco/lib/src/models/override.dart b/packages/disco/lib/src/models/override.dart
new file mode 100644
index 00000000..4003098b
--- /dev/null
+++ b/packages/disco/lib/src/models/override.dart
@@ -0,0 +1,45 @@
+part of '../disco_internal.dart';
+
+/// A declarative configuration holding all the data needed to
+/// construct and register a mock provider within a
+/// [ProviderScope].
+///
+/// Concretely, it is either one of:
+/// - [ProviderOverride]
+/// - [ArgProviderOverride].
+@immutable
+sealed class Override {
+ Override._();
+}
+
+/// Override that, if inserted into the widget tree, gives [_mockArgProvider]
+/// precedence over [_originalArgProvider].
+@immutable
+class ArgProviderOverride extends Override {
+ ArgProviderOverride._withArgProvider(
+ this._originalArgProvider,
+ this._mockArgProvider,
+ ) : super._();
+
+ /// The reference of the argument provider to override.
+ final ArgProvider _originalArgProvider;
+
+ /// The reference of the argument provider override.
+ final ArgProvider _mockArgProvider;
+}
+
+/// Override that, if inserted into the widget tree, gives [_mockProvider]
+/// precedence over [_originalProvider].
+@immutable
+class ProviderOverride extends Override {
+ ProviderOverride._withProvider(
+ this._originalProvider,
+ this._mockProvider,
+ ) : super._();
+
+ /// The reference of the provider to override.
+ final Provider _originalProvider;
+
+ /// The reference of the provider override.
+ final Provider _mockProvider;
+}
diff --git a/packages/disco/lib/src/models/overrides/override.dart b/packages/disco/lib/src/models/overrides/override.dart
deleted file mode 100644
index a71c9571..00000000
--- a/packages/disco/lib/src/models/overrides/override.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-part of '../../disco_internal.dart';
-
-/// Either a [ProviderOverride] or an [ArgProviderOverride].
-@immutable
-sealed class Override {
- Override._();
-}
diff --git a/packages/disco/lib/src/models/overrides/provider_argument_override.dart b/packages/disco/lib/src/models/overrides/provider_argument_override.dart
deleted file mode 100644
index 77dfbca8..00000000
--- a/packages/disco/lib/src/models/overrides/provider_argument_override.dart
+++ /dev/null
@@ -1,28 +0,0 @@
-part of '../../disco_internal.dart';
-
-/// Override that, if inserted into the widget tree, takes precedence over
-/// [_argProvider].
-@immutable
-class ArgProviderOverride extends Override {
- ArgProviderOverride._(this._argProvider, T value, {this.debugName})
- : _value = value,
- super._();
-
- /// The reference of the argument provider to override.
- final ArgProvider _argProvider;
-
- /// The overridden value.
- final T _value;
-
- // Utils leveraged by ProviderScope -----------------------------------------
-
- /// Given an argument, creates a [Provider] with that argument.
- /// This method is used internally by [ProviderScope].
- Provider _generateIntermediateProvider() => Provider(
- (_) => _value,
- lazy: false,
- );
-
- /// {@macro Provider.debugName}
- final String? debugName;
-}
diff --git a/packages/disco/lib/src/models/overrides/provider_override.dart b/packages/disco/lib/src/models/overrides/provider_override.dart
deleted file mode 100644
index 166f9080..00000000
--- a/packages/disco/lib/src/models/overrides/provider_override.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-part of '../../disco_internal.dart';
-
-/// Override that, if inserted into the widget tree, takes precedence over
-/// [_provider].
-@immutable
-class ProviderOverride extends Override {
- ProviderOverride._(
- this._provider,
- T value,
- ) : _value = value,
- super._();
-
- /// The reference of the provider to override.
- final Provider _provider;
-
- final T _value;
-
- // Utils leveraged by ProviderScope -----------------------------------------
-
- /// Creates a [Provider].
- /// This method is used internally by [ProviderScope].
- Provider _generateIntermediateProvider() => Provider(
- (_) => _value,
- lazy: false,
- );
-}
diff --git a/packages/disco/lib/src/models/providers/provider_argument.dart b/packages/disco/lib/src/models/providers/arg_provider.dart
similarity index 64%
rename from packages/disco/lib/src/models/providers/provider_argument.dart
rename to packages/disco/lib/src/models/providers/arg_provider.dart
index c64d52e0..f2c4a553 100644
--- a/packages/disco/lib/src/models/providers/provider_argument.dart
+++ b/packages/disco/lib/src/models/providers/arg_provider.dart
@@ -17,14 +17,9 @@ class ArgProvider {
ArgProvider._(
CreateArgProviderValueFn create, {
DisposeProviderValueFn? dispose,
- bool? lazy,
this.debugName,
}) : _createValue = create,
- _disposeValue = dispose,
- _lazy = lazy ?? DiscoConfig.lazy;
-
- /// {@macro Provider.lazy}
- final bool _lazy;
+ _disposeValue = dispose;
/// {@macro Provider.create}
final CreateArgProviderValueFn _createValue;
@@ -33,13 +28,26 @@ class ArgProvider {
final DisposeProviderValueFn? _disposeValue;
// ---
- // Overrides
+ // Override
// ---
- /// {@macro Provider.overrideWithValue}
+ /// {@macro Provider.overrideWithProvider}
+ @visibleForTesting
+ ArgProviderOverride overrideWith(
+ ArgProvider override,
+ ) => ArgProviderOverride._withArgProvider(this, override);
+
+ /// Deprecated: Use [overrideWith] instead.
+ ///
+ /// This method is deprecated and will be removed in a future version.
+ /// Use `provider.overrideWith(Provider.withArgument((_, __) => value))`
+ /// instead.
+ @Deprecated(
+ 'Use overrideWith(Provider.withArgument((_, __) => value)) instead',
+ )
@visibleForTesting
ArgProviderOverride overrideWithValue(T value) =>
- ArgProviderOverride._(this, value, debugName: debugName);
+ overrideWith(Provider.withArgument((_, __) => value));
// ---
// DI methods
@@ -63,11 +71,15 @@ class ArgProvider {
// Utils leveraged by ProviderScope
// ---
- /// It creates an [InstantiableArgProvider] with the passed argument.
+ /// It creates an [ArgProviderValueBinding] with the passed argument.
/// This ensures that an [ArgProvider] inserted into the widget tree always
/// has an initial argument and, thus, can be created.
- InstantiableArgProvider call(A arg) {
- return InstantiableArgProvider._(this, arg);
+ /// You should interpret this as following: this method creates all necessary
+ /// "instructions"/"data" for [ProviderScope] to actually generate an
+ /// intermediate provider, and thus also an actual value
+ /// (note that the value is computed lazily).
+ ArgProviderValueBinding call(A arg) {
+ return ArgProviderValueBinding._(this, arg);
}
/// Returns the type of the value
@@ -81,23 +93,8 @@ class ArgProvider {
Provider _generateIntermediateProvider(A arg) => Provider(
(context) => _createValue(context, arg),
dispose: _disposeValue,
- lazy: _lazy,
);
/// {@macro Provider.debugName}
final String? debugName;
}
-
-/// {@template InstantiableArgProvider}
-/// An instance of this class is needed to insert an [ArgProvider] into the
-/// widget tree. This ensures that an initial argument is always present and,
-/// thus, the [ArgProvider] can be correctly created.
-/// {@endtemplate}
-@immutable
-class InstantiableArgProvider
- extends InstantiableProvider {
- /// {@macro InstantiableArgProvider}
- InstantiableArgProvider._(this._argProvider, this._arg) : super._();
- final ArgProvider _argProvider;
- final A _arg;
-}
diff --git a/packages/disco/lib/src/models/providers/instantiable_provider.dart b/packages/disco/lib/src/models/providers/instantiable_provider.dart
deleted file mode 100644
index 7dea2f96..00000000
--- a/packages/disco/lib/src/models/providers/instantiable_provider.dart
+++ /dev/null
@@ -1,8 +0,0 @@
-part of '../../disco_internal.dart';
-
-/// Either a [Provider] or an [InstantiableArgProvider] (i.e. an [ArgProvider]
-/// with its argument).
-@immutable
-sealed class InstantiableProvider {
- InstantiableProvider._();
-}
diff --git a/packages/disco/lib/src/models/providers/provider.dart b/packages/disco/lib/src/models/providers/provider.dart
index c1ffeaa5..2a27eda0 100644
--- a/packages/disco/lib/src/models/providers/provider.dart
+++ b/packages/disco/lib/src/models/providers/provider.dart
@@ -14,15 +14,27 @@ typedef DisposeProviderValueFn = void Function(T value);
/// such as instantiating a BLoC.
///
/// Provider is the equivalent of a State.initState combined with State.dispose.
-/// [_createValue] is called only once in State.initState.
-/// The `create` callback is lazily called. It is called the first time the
-/// value is read, instead of the first time Provider is inserted in the widget
-/// tree.
-/// This behavior can be disabled by passing [_lazy] false.
+/// The `create` callback is always called lazily, i.e. the first time the value
+/// is injected, and not when the provider is inserted into the widget tree.
+///
+/// > If you need a value to be created as soon as its [ProviderScope] is
+/// > mounted, inject it in a widget placed right below the scope:
+/// >
+/// > ```dart
+/// > ProviderScope(
+/// > providers: [myProvider()],
+/// > child: Builder(
+/// > builder: (context) {
+/// > myProvider.of(context);
+/// > return const MyChild();
+/// > },
+/// > ),
+/// > )
+/// > ```
///
/// {@endtemplate}
@immutable
-class Provider extends InstantiableProvider {
+class Provider {
//! NB: do not make the constructor `const`, since that would give the same
//! hash code to different instances of `Provider` with the same generic
//! type.
@@ -34,33 +46,16 @@ class Provider extends InstantiableProvider {
/// {@macro Provider.dispose}
DisposeProviderValueFn? dispose,
-
- /// {@macro Provider.lazy}
- bool? lazy,
this.debugName,
}) : _createValue = create,
- _disposeValue = dispose,
- _lazy = lazy ?? DiscoConfig.lazy,
- super._();
+ _disposeValue = dispose;
/// {@macro arg-provider}
static ArgProvider withArgument(
CreateArgProviderValueFn create, {
DisposeProviderValueFn? dispose,
- bool lazy = true,
String? debugName,
- }) =>
- ArgProvider._(create, dispose: dispose, lazy: lazy, debugName: debugName);
-
- /// {@template Provider.lazy}
- /// Makes the creation of the provided value lazy. defaults to true.
- ///
- /// > The provider itself is not lazily created, only its contained value.
- ///
- /// if this value is true, the provider's value will be created only when
- /// retrieved from descendants for the first time.
- /// {@endtemplate}
- final bool _lazy;
+ }) => ArgProvider._(create, dispose: dispose, debugName: debugName);
/// {@template Provider.create}
/// The function called to create the element.
@@ -74,15 +69,25 @@ class Provider extends InstantiableProvider {
/// {@endtemplate}
final DisposeProviderValueFn? _disposeValue;
- // Overrides ----------------------------------------------------------------
+ // Override -----------------------------------------------------------------
- /// {@template Provider.overrideWithValue}
+ /// {@template Provider.overrideWithProvider}
/// It creates an override of this provider to be passed to
/// [ProviderScopeOverride].
/// {@endtemplate}
@visibleForTesting
+ ProviderOverride overrideWith(
+ Provider override,
+ ) => ProviderOverride._withProvider(this, override);
+
+ /// Deprecated: Use [overrideWith] instead.
+ ///
+ /// This method is deprecated and will be removed in a future version.
+ /// Use `provider.overrideWith(Provider((_) => value))` instead.
+ @Deprecated('Use overrideWith(Provider((_) => value)) instead')
+ @visibleForTesting
ProviderOverride overrideWithValue(T value) =>
- ProviderOverride._(this, value);
+ overrideWith(Provider((_) => value));
// DI methods ---------------------------------------------------------------
@@ -119,6 +124,28 @@ class Provider extends InstantiableProvider {
_disposeValue?.call(value as T);
}
+ /// This method creates a [ProviderValueBinding]. You should interpret this
+ /// as following: this method creates all necessary "instructions"/"data" for
+ /// [ProviderScope] to actually generate an intermediate provider, and thus
+ /// also an actual value (note that the value is computed lazily).
+ // ignore: use_to_and_as_if_applicable
+ ProviderValueBinding call() {
+ return ProviderValueBinding._(this);
+ }
+
+ /// Creates a new [Provider] behaving exactly like this one.
+ ///
+ /// This method is used internally by [ProviderScope] to generate the
+ /// intermediate provider of an overridden provider. Generating a fresh
+ /// instance guarantees that the same mock can override more than one
+ /// provider without the resulting values being shared, since the values are
+ /// keyed by their intermediate provider.
+ Provider _generateIntermediateProvider() => Provider(
+ _createValue,
+ dispose: _disposeValue,
+ debugName: debugName,
+ );
+
/// Returns the type of the value.
Type get _valueType => T;
diff --git a/packages/disco/lib/src/models/value_binding.dart b/packages/disco/lib/src/models/value_binding.dart
new file mode 100644
index 00000000..d758dbe9
--- /dev/null
+++ b/packages/disco/lib/src/models/value_binding.dart
@@ -0,0 +1,44 @@
+part of '../disco_internal.dart';
+
+/// A declarative configuration holding all the data needed to construct and
+/// register an intermediate provider - and thus a lazy value - within a
+/// [ProviderScope].
+/// It specifies all needed information (e.g., the argument in case of an
+/// [ArgProvider]).
+///
+/// This class acts as a lightweight blueprint. No actual value instantiation
+/// or state evaluation occurs here; instead, value instantiation happens in
+/// the [ProviderScope].
+///
+/// Concretely, it is either one of:
+/// - [ProviderValueBinding]
+/// - [ArgProviderValueBinding]
+@immutable
+sealed class ValueBinding {
+ ValueBinding._();
+}
+
+/// {@template ArgProviderValueBinding}
+/// Binds an [ArgProvider] along with its required initial argument [A]
+/// for registration in a [ProviderScope].
+/// {@endtemplate}
+@immutable
+class ArgProviderValueBinding extends ValueBinding {
+ /// {@macro ArgProviderValueBinding}
+ ArgProviderValueBinding._(this._argProvider, this._arg) : super._();
+ final ArgProvider _argProvider;
+ final A _arg;
+}
+
+/// {@template ProviderValueBinding}
+/// Binds a standard [Provider] for registration in a [ProviderScope].
+///
+/// While a standard provider does not require arguments, wrapping it in a
+/// [ProviderValueBinding] provides a uniform API for all entries in the
+/// widget tree alongside [ArgProviderValueBinding].
+/// {@endtemplate}
+class ProviderValueBinding extends ValueBinding {
+ /// {@macro ProviderValueBinding}
+ ProviderValueBinding._(this._provider) : super._();
+ final Provider _provider;
+}
diff --git a/packages/disco/lib/src/utils/disco_config.dart b/packages/disco/lib/src/utils/disco_config.dart
deleted file mode 100644
index 835a1a72..00000000
--- a/packages/disco/lib/src/utils/disco_config.dart
+++ /dev/null
@@ -1,7 +0,0 @@
-part of '../disco_internal.dart';
-
-/// The global preferences for the Disco package.
-abstract final class DiscoConfig {
- /// {@macro Provider.lazy}
- static bool lazy = true;
-}
diff --git a/packages/disco/lib/src/utils/extensions.dart b/packages/disco/lib/src/utils/extensions.dart
index fa87e112..db19d1b1 100644
--- a/packages/disco/lib/src/utils/extensions.dart
+++ b/packages/disco/lib/src/utils/extensions.dart
@@ -2,7 +2,7 @@ part of '../disco_internal.dart';
extension _DebugNameProvider on Provider {
// Returns a debug name for the provider.
- String? get _debugName {
+ String get _debugName {
var s = 'Provider<$_valueType>';
if (debugName != null) s += '(name: $debugName)';
return s;
@@ -11,7 +11,7 @@ extension _DebugNameProvider on Provider {
extension _DebugNameArgProvider on ArgProvider {
// Returns a debug name for the provider with arguments.
- String? get _debugName {
+ String get _debugName {
var s = 'ArgProvider<$_valueType, $_argumentType>';
if (debugName != null) s += '(name: $debugName)';
return s;
diff --git a/packages/disco/lib/src/widgets/provider_scope.dart b/packages/disco/lib/src/widgets/provider_scope.dart
index 287409d4..f97d0653 100644
--- a/packages/disco/lib/src/widgets/provider_scope.dart
+++ b/packages/disco/lib/src/widgets/provider_scope.dart
@@ -5,111 +5,75 @@
part of '../disco_internal.dart';
/// {@template ProviderScope}
-/// Provides the passed [providers] to descendants (i.e. what is in [child]).
+/// Provides the passed [_providers] to descendants (i.e. what is in [child]).
/// {@endtemplate}
@immutable
class ProviderScope extends StatefulWidget {
/// {@macro ProviderScope}
const ProviderScope({
required this.child,
- required List this.providers,
+ required List providers,
super.key,
- }) : overrides = null;
+ }) : _providers = providers,
+ _overrides = null;
const ProviderScope._overrides({
required this.child,
- required List this.overrides,
+ required List overrides,
super.key,
- }) : providers = null;
+ }) : _overrides = overrides,
+ _providers = null;
/// {@template ProviderScope.child}
- /// The widget child that gets access to the [providers].
+ /// The widget child that gets access to the providers.
/// {@endtemplate}
final Widget child;
- /// All the providers provided to all the descendants of [ProviderScope].
- final List? providers;
+ /// All the providers provided to all the descendants of this [ProviderScope].
+ ///
+ /// Exactly one of [_providers] and [_overrides] is non-null.
+ final List? _providers;
- /// All the overrides provided to all the descendants of
+ /// All the overrides provided to all the descendants of a
/// [ProviderScopeOverride].
- final List? overrides;
+ ///
+ /// Exactly one of [_providers] and [_overrides] is non-null.
+ final List? _overrides;
@override
State createState() => ProviderScopeState();
- /// {@template _findState}
- /// Finds the first [ProviderScopeState] ancestor that satisfies the given
- /// [id].
- /// {@endtemplate}
- static ProviderScopeState? _findState(
+ /// Finds the first [ProviderScopeState] ancestor providing the given ID.
+ ///
+ /// Exactly one of [providerId] and [argProviderId] must be given.
+ ///
+ /// NB: the scope providing an ID is always looked up through the widget tree,
+ /// even while that scope is creating its own values. Since the internal
+ /// [ProviderScope] of a [ProviderScopeOverride] takes part in the widget tree
+ /// as well, this is also what makes the overrides apply to the providers
+ /// depending on an overridden provider.
+ static ProviderScopeState? _findState(
BuildContext context, {
- required Provider id,
+ Provider? providerId,
+ ArgProvider? argProviderId,
}) {
- // try and find the override first
- final providerScopeOverride = ProviderScopeOverrideState.maybeOf(context);
- if (providerScopeOverride != null) {
- final state = providerScopeOverride.providerScopeState;
- if (state.isProviderInScope(id)) return state;
- }
-
- return _InheritedProvider.inheritFromNearest(context, id, null)?.state;
+ return _InheritedProvider.findNearestProviding(
+ context,
+ providerId,
+ argProviderId,
+ )?.state;
}
/// Helper method to handle common logic between Provider and ArgProvider
- /// access during initialization and lazy creation.
+ /// access during lazy creation.
/// [id] can be either a `Provider` or an `ArgProvider`.
static T? _getOrCreateValue({
required BuildContext context,
required ID id,
- required bool Function(ProviderScopeState, ID) isInScope,
- required int? Function(ProviderScopeState, ID) getIndex,
- required Provider? Function(ProviderScopeState, ID) getProviderId,
+ required T? Function(ProviderScopeState, ID) getCreatedValue,
required ProviderScopeState? Function(BuildContext, ID) findState,
required T Function(ProviderScopeState, ID, BuildContext) createValue,
}) {
- // STEP 1: Check if we're in the middle of initializing a scope
- final initializingScope = ProviderScopeState._currentlyInitializingScope;
- if (initializingScope != null) {
- // Check if the requested provider is in the CURRENT scope being
- // initialized
- if (isInScope(initializingScope, id)) {
- // Found in current scope! Now validate ordering.
- final requestedIndex = getIndex(initializingScope, id);
- final currentIndex = initializingScope._currentlyCreatingProviderIndex;
-
- // If we're currently creating a provider, validate it's not a
- // forward ref
- if (currentIndex != null && requestedIndex != null) {
- if (requestedIndex >= currentIndex) {
- // Forward reference detected!
- final currentProvider =
- initializingScope._currentlyCreatingProvider;
- assert(
- currentProvider != null,
- 'Current provider should be set during initialization',
- );
- throw ProviderForwardReferenceError(
- requestedProvider: id,
- currentProvider: currentProvider!,
- );
- }
- }
-
- // Valid same-scope access to an earlier provider
- // Check if already created
- final providerId = getProviderId(initializingScope, id);
- final createdProvider =
- initializingScope.createdProviderValues[providerId];
- // coverage:ignore-start
- if (createdProvider != null) return createdProvider as T;
- // coverage:ignore-end
-
- // Not created yet - create it now (for lazy providers)
- return createValue(initializingScope, id, context);
- }
- }
-
- // STEP 2: Not in current scope or not initializing - search ancestors
// Try to find the provider in the current widget tree.
var state = findState(context, id);
// If the state has not been found yet, try to find it by using the
@@ -121,21 +85,21 @@ class ProviderScope extends StatefulWidget {
}
}
if (state == null) return null;
- final providerId = getProviderId(state, id);
- final createdProvider = state.createdProviderValues[providerId];
- if (createdProvider != null) return createdProvider as T;
- // if the provider is not already present, create it lazily
+
+ final createdValue = getCreatedValue(state, id);
+ if (createdValue != null) return createdValue;
+ // if the value has not been created yet, create it lazily
return createValue(state, id, context);
}
- /// {@template _getOrCreateProvider}
+ /// {@template _getOrCreateProviderValue}
/// Tries to find the provided value associated to [id].
///
/// If the [id] is not found in any [ProviderScope], this function
/// returns null.
///
/// In case the [id] is found in some [ProviderScope], but the find fails
- /// (no associated value in [ProviderScopeState.createdProviderValues]),
+ /// (no associated value in [ProviderScopeState._createdValues]),
/// the provider's value gets created.
/// {@endtemplate}
static T? _getOrCreateProviderValue(
@@ -145,31 +109,14 @@ class ProviderScope extends StatefulWidget {
return _getOrCreateValue>(
context: context,
id: id,
- isInScope: (scope, id) => scope.isProviderInScope(id),
- getIndex: (scope, id) => scope._providerIndices[id],
- getProviderId: (scope, id) => id,
- findState: (context, id) => _findState(context, id: id),
+ getCreatedValue: (scope, id) => scope._getCreatedProviderValue(id) as T?,
+ findState: (context, id) => _findState(context, providerId: id),
createValue: (scope, id, context) =>
- scope.createProviderValue(id, context) as T,
+ scope._createProviderValue(id, context) as T,
);
}
- /// {@macro _findState}
- static ProviderScopeState? _findStateForArgProvider(
- BuildContext context, {
- required ArgProvider id,
- }) {
- // try finding the override first
- final providerScopeOverride = ProviderScopeOverrideState.maybeOf(context);
- if (providerScopeOverride != null) {
- final state = providerScopeOverride.providerScopeState;
- if (state.isArgProviderInScope(id)) return state;
- }
-
- return _InheritedProvider.inheritFromNearest(context, null, id)?.state;
- }
-
- /// {@macro _getOrCreateProvider}
+ /// {@macro _getOrCreateProviderValue}
static T? _getOrCreateArgProviderValue(
BuildContext context, {
required ArgProvider id,
@@ -177,13 +124,11 @@ class ProviderScope extends StatefulWidget {
return _getOrCreateValue>(
context: context,
id: id,
- isInScope: (scope, id) => scope.isArgProviderInScope(id),
- getIndex: (scope, id) => scope._argProviderIndices[id],
- getProviderId: (scope, id) => scope.allArgProvidersInScope[id],
- findState: (context, id) =>
- _findStateForArgProvider(context, id: id),
+ getCreatedValue: (scope, id) =>
+ scope._getCreatedArgProviderValue(id) as T?,
+ findState: (context, id) => _findState(context, argProviderId: id),
createValue: (scope, id, context) =>
- scope.createProviderValueForArgProvider(id, context) as T,
+ scope._createProviderValueForArgProvider(id, context) as T,
);
}
}
@@ -191,76 +136,191 @@ class ProviderScope extends StatefulWidget {
/// The state of the [ProviderScope] widget
@protected
class ProviderScopeState extends State {
- /// Stores all the argument providers in the current scope. The values are
- /// intermediate providers, which are used as internal IDs by
- /// [createdProviderValues].
- final allArgProvidersInScope = HashMap();
+ // There are three layers of providers:
+ //
+ // 1. the top-level providers, i.e. the ones defined by the user. They are
+ // never used to create any value: they only act as type-safe IDs.
+ // 2. the intermediate providers, i.e. the providers actually used to create
+ // the values. An intermediate provider is generated (or regenerated, in
+ // case of an override) for every top-level provider inserted into this
+ // scope. For a [Provider], the intermediate provider is either the
+ // top-level provider itself or a copy of its mock; for an [ArgProvider],
+ // the intermediate provider is a [Provider] generated by combining the
+ // (possibly overridden) argument provider with the argument given in the
+ // widget tree.
+ // 3. the values, which are created by the intermediate providers and are
+ // keyed by them.
+
+ /// Stores all the argument providers in the current scope. The keys are the
+ /// top-level argument providers, while the values are the intermediate
+ /// providers, which are used as internal IDs by [_createdValues].
+ final _allArgProvidersInScope = HashMap();
/// Stores all the providers without argument in the current scope.
- /// The values are intermediate providers, which are used as internal IDs
- /// by [createdProviderValues].
- final allProvidersInScope = HashMap();
-
- /// Stores all the created values (associated to the providers).
- /// The keys are the intermediate providers (which are not necessarily the
- /// globally defined providers), while the values are the provided values.
- final createdProviderValues = HashMap();
-
- /// Track the scope currently being initialized. This enables same-scope
- /// provider access during initialization.
- static ProviderScopeState? _currentlyInitializingScope;
-
- /// Map each provider to its index in the original providers list.
- /// Used to enforce ordering constraints during same-scope access.
- final _providerIndices = HashMap();
+ /// The keys are the top-level providers, while the values are the
+ /// intermediate providers, which are used as internal IDs by
+ /// [_createdValues].
+ final _allProvidersInScope = HashMap();
- /// Map each ArgProvider to its index in the original providers list.
- /// Used to enforce ordering constraints during same-scope access.
- final _argProviderIndices = HashMap();
+ /// Stores the providers overridden by a [ProviderScopeOverride].
+ ///
+ /// This map is only filled for the internal [ProviderScope] of a
+ /// [ProviderScopeOverride]. It maps a top-level provider to the provider
+ /// that has to be used in its place.
+ ///
+ /// Every [ProviderScope] looks these overrides up while generating its own
+ /// intermediate providers, so that the value of an overridden provider lives
+ /// in the very same scope where the value of the original provider would have
+ /// lived, and therefore shares its exact lifecycle.
+ final _overriddenProviders = HashMap();
- /// The index of the provider currently being created during initialization.
- /// Null when not initializing. Used to detect forward/circular references.
- int? _currentlyCreatingProviderIndex;
+ /// Stores the argument providers overridden by a [ProviderScopeOverride].
+ ///
+ /// This map is only filled for the internal [ProviderScope] of a
+ /// [ProviderScopeOverride]. It maps a top-level argument provider to the
+ /// argument provider that has to be used in its place.
+ ///
+ /// Differently from [ProviderOverride]s, an [ArgProviderOverride] cannot be
+ /// instantiated here, since the argument is only known where the argument
+ /// provider is inserted into the widget tree. Therefore, the overrides are
+ /// only registered here and every [ProviderScope] looks them up while
+ /// generating its intermediate providers.
+ ///
+ /// NB: differently from [_overriddenProviders], an overridden argument
+ /// provider cannot be provided by this scope as a fallback, since no argument
+ /// is available here. Therefore, only the [ProviderScope]s that are
+ /// descendants of the [ProviderScopeOverride] are affected.
+ final _overriddenArgProviders = HashMap();
+
+ /// Stores all the values created by this scope, no matter whether they come
+ /// from a [Provider] or from an [ArgProvider].
+ ///
+ /// The keys are the intermediate providers (which are not necessarily the
+ /// top-level providers), while the values are the provided values.
+ ///
+ /// NB: this map preserves the insertion order, which is the order in which
+ /// the values have been created. [dispose] relies on it.
+ final _createdValues = {};
- /// The provider object currently being created during initialization.
- /// Null when not initializing. Used for error reporting.
- /// Can be either a Provider or ArgProvider instance.
- Object? _currentlyCreatingProvider;
+ /// The top-level providers whose values are currently being created by this
+ /// scope, in order of creation. Used to detect circular dependencies.
+ ///
+ /// Every element is either a [Provider] or an [ArgProvider].
+ final _idsBeingCreated = [];
@override
void initState() {
super.initState();
- // Set this scope as currently initializing to enable same-scope access
- _currentlyInitializingScope = this;
+ final providers = widget._providers;
+ if (providers != null) {
+ _initializeProviders(providers);
+ } else {
+ _initializeOverrides(widget._overrides!);
+ }
+ }
- try {
- if (widget.providers != null) {
- _initializeProviders(widget.providers!);
+ @override
+ void didUpdateWidget(ProviderScope oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ assert(_debugCheckScopeDidNotChange(oldWidget), '');
+ }
+
+ /// Checks that the set of providers of this scope has not changed.
+ ///
+ /// The providers (or the overrides) of a [ProviderScope] are read exactly
+ /// once, when the scope is mounted. Inserting or removing one on a later
+ /// rebuild would silently have no effect, therefore it is reported as an
+ /// error in debug mode.
+ ///
+ /// NB: the error is *reported* and not thrown. Throwing here would abort the
+ /// update of the element tree halfway through, which makes the framework fail
+ /// again later on, in a much more confusing way. Reporting keeps this scope
+ /// working with the providers it has been mounted with, which is exactly what
+ /// the change amounts to.
+ ///
+ /// NB: only the *identity* of the providers is compared. Giving an argument
+ /// provider a different argument on a rebuild is deliberately allowed, since
+ /// the argument is often rebuilt along with the widget; the initial argument
+ /// keeps winning, as documented in
+ /// .
+ bool _debugCheckScopeDidNotChange(ProviderScope oldWidget) {
+ // The identity of every provider (or overridden provider) of a scope.
+ Set describe(ProviderScope scope) {
+ final ids = {};
+
+ final providers = scope._providers;
+ if (providers != null) {
+ for (final item in providers) {
+ if (item is ProviderValueBinding) {
+ ids.add(item._provider);
+ } else if (item is ArgProviderValueBinding) {
+ ids.add(item._argProvider);
+ }
+ }
+ return ids;
}
- if (widget.overrides != null) {
- _initializeOverrides(widget.overrides!);
+
+ for (final item in scope._overrides!) {
+ if (item is ProviderOverride) {
+ ids.add(item._originalProvider);
+ } else if (item is ArgProviderOverride) {
+ ids.add(item._originalArgProvider);
+ }
}
- } finally {
- _currentlyInitializingScope = null;
- _currentlyCreatingProviderIndex = null;
- _currentlyCreatingProvider = null;
+ return ids;
}
+
+ final oldIds = describe(oldWidget);
+ final newIds = describe(widget);
+
+ if (oldIds.length != newIds.length || !newIds.containsAll(oldIds)) {
+ FlutterError.reportError(
+ FlutterErrorDetails(
+ exception: FlutterError.fromParts([
+ ErrorSummary(
+ 'The providers of a ProviderScope changed after it had been '
+ 'mounted.',
+ ),
+ ErrorDescription(
+ 'The providers (or the overrides) of a ProviderScope are read '
+ 'exactly once, when the scope is mounted. Inserting a provider '
+ 'into the list, or removing one from it, on a later rebuild '
+ 'therefore has no effect, and it usually surfaces much later as '
+ 'a ProviderWithoutScopeError.',
+ ),
+ ErrorHint(
+ 'Provide a fixed set of providers, and use a nested '
+ 'ProviderScope for the ones whose presence depends on the state '
+ 'of your widget. If you really need this scope to be rebuilt '
+ 'from scratch, give it a different key instead: that disposes '
+ 'its values and creates them again.',
+ ),
+ ]),
+ library: 'disco',
+ context: ErrorDescription('while updating a ProviderScope'),
+ ),
+ );
+ }
+
+ return true;
}
/// Validates that there are no duplicate providers in the list.
- void _validateProvidersUniqueness(List allProviders) {
+ void _validateProvidersUniqueness(
+ List allProviders,
+ ) {
assert(
() {
final providerIds = {};
final argProviderIds = {};
for (final item in allProviders) {
- if (item is Provider) {
- if (!providerIds.add(item)) {
+ if (item is ProviderValueBinding) {
+ if (!providerIds.add(item._provider)) {
throw MultipleProviderOfSameInstance();
}
- } else if (item is InstantiableArgProvider) {
+ } else if (item is ArgProviderValueBinding) {
if (!argProviderIds.add(item._argProvider)) {
throw MultipleProviderOfSameInstance();
}
@@ -272,81 +332,50 @@ class ProviderScopeState extends State {
);
}
- /// PHASE 1: Registers all providers and tracks their indices.
- /// This must be done before creating any providers so that
- /// isProviderInScope() works correctly during creation.
- void _registerAllProviders(List allProviders) {
- for (var i = 0; i < allProviders.length; i++) {
- final item = allProviders[i];
-
- if (item is Provider) {
- final provider = item;
- final id = provider;
-
- // Track original index for ordering validation
- _providerIndices[id] = i;
-
- // In this case, the provider put in scope can be the ID itself.
- allProvidersInScope[id] = provider;
- } else if (item is InstantiableArgProvider) {
- final instantiableArgProvider = item;
- final id = instantiableArgProvider._argProvider;
-
- // Track original index for ordering validation
- _argProviderIndices[id] = i;
-
- final provider = instantiableArgProvider._argProvider
- ._generateIntermediateProvider(
- instantiableArgProvider._arg,
- );
- allArgProvidersInScope[id] = provider;
- }
- }
- }
-
- /// PHASE 2: Creates non-lazy providers.
- /// Now that all providers are registered, we can create them.
- void _createNonLazyProviders(List allProviders) {
- for (var i = 0; i < allProviders.length; i++) {
- final item = allProviders[i];
-
- if (item is Provider) {
- final provider = item;
- final id = provider;
-
- // create non lazy providers.
- if (!provider._lazy) {
- _currentlyCreatingProviderIndex = i;
- _currentlyCreatingProvider = id;
- createdProviderValues[id] = provider._createValue(context);
- _currentlyCreatingProviderIndex = null;
- _currentlyCreatingProvider = null;
- }
- } else if (item is InstantiableArgProvider) {
- final instantiableArgProvider = item;
- final id = instantiableArgProvider._argProvider;
-
- // create non lazy providers.
- if (!instantiableArgProvider._argProvider._lazy) {
- _currentlyCreatingProviderIndex = i;
- _currentlyCreatingProvider = id;
- createdProviderValues[allArgProvidersInScope[id]!] =
- allArgProvidersInScope[id]!._createValue(context);
- _currentlyCreatingProviderIndex = null;
- _currentlyCreatingProvider = null;
- }
+ /// Registers all providers, i.e. it generates their intermediate providers.
+ ///
+ /// This is done as soon as the scope is mounted, before any value exists, so
+ /// that a provider can inject the other providers of its own scope no matter
+ /// the order in which they are declared.
+ void _registerAllProviders(List allProviders) {
+ // The overrides of a ProviderScopeOverride, if present. They are needed to
+ // regenerate the intermediate providers of the overridden providers.
+ final overridesScope = ProviderScopeOverrideState.maybeOf(
+ context,
+ )?._providerScopeState;
+
+ for (final item in allProviders) {
+ if (item is ProviderValueBinding) {
+ final id = item._provider;
+
+ // If this provider is overridden, its mock generates the intermediate
+ // provider; otherwise the top-level provider can act as the
+ // intermediate provider itself.
+ final mock = overridesScope?._getOverriddenProvider(id);
+ _allProvidersInScope[id] = mock?._generateIntermediateProvider() ?? id;
+ } else if (item is ArgProviderValueBinding) {
+ final id = item._argProvider;
+
+ // The argument provider generating the intermediate provider is either
+ // the top-level one or, if overridden, its mock.
+ final argProvider = overridesScope?._getOverriddenArgProvider(id) ?? id;
+
+ _allArgProvidersInScope[id] = argProvider._generateIntermediateProvider(
+ item._arg,
+ );
}
}
}
- /// Initializes providers by validating, registering, and creating them.
- void _initializeProviders(List allProviders) {
+ /// Initializes providers by validating and registering them. Their values are
+ /// always created lazily, i.e. the first time they are injected.
+ void _initializeProviders(List allProviders) {
_validateProvidersUniqueness(allProviders);
_registerAllProviders(allProviders);
- _createNonLazyProviders(allProviders);
}
- /// Processes provider overrides by validating uniqueness and creating them.
+ /// Processes provider overrides by validating uniqueness and registering
+ /// them.
void _processProviderOverrides(
List> providerOverrides,
) {
@@ -355,7 +384,7 @@ class ProviderScopeState extends State {
// check if there are multiple providers of the same type
final ids = [];
for (final override in providerOverrides) {
- final id = override._provider; // the instance of the provider
+ final id = override._originalProvider; // the instance of the provider
if (ids.contains(id)) {
throw MultipleProviderOverrideOfSameInstance();
}
@@ -367,19 +396,25 @@ class ProviderScopeState extends State {
);
for (final override in providerOverrides) {
- final id = override._provider;
-
- allProvidersInScope[id] = override._generateIntermediateProvider();
-
- // create providers (they are never lazy in the case of overrides)
- createdProviderValues[id] = allProvidersInScope[id]!._createValue(
- context,
- );
+ final id = override._originalProvider;
+ final mock = override._mockProvider;
+
+ // The mock is registered, so that every ProviderScope below can
+ // regenerate its intermediate provider out of it. This is what makes the
+ // value of an overridden provider live exactly where the value of the
+ // original provider would have lived.
+ _overriddenProviders[id] = mock;
+
+ // The mock is also provided by this scope, so that an override works even
+ // if no ProviderScope below provides the original provider at all. In
+ // that case only, the value lives here.
+ _allProvidersInScope[id] = mock._generateIntermediateProvider();
}
}
- /// Processes arg provider overrides by validating uniqueness and creating
- /// them.
+ /// Processes arg provider overrides by validating uniqueness and registering
+ /// them, so that the [ProviderScope]s below can generate their intermediate
+ /// providers out of them.
void _processArgProviderOverrides(
List> argProviderOverrides,
) {
@@ -388,7 +423,8 @@ class ProviderScopeState extends State {
// check if there are multiple providers of the same type
final ids = [];
for (final override in argProviderOverrides) {
- final id = override._argProvider; // the instance of the provider
+ final id =
+ override._originalArgProvider; // the instance of the provider
if (ids.contains(id)) {
throw MultipleProviderOverrideOfSameInstance();
}
@@ -400,14 +436,13 @@ class ProviderScopeState extends State {
);
for (final override in argProviderOverrides) {
- final id = override._argProvider;
-
- allArgProvidersInScope[id] = override._generateIntermediateProvider();
+ final id = override._originalArgProvider;
- // create providers (they are never lazy in the case of overrides)
- final intermediateId = allArgProvidersInScope[id]!;
- createdProviderValues[intermediateId] = allArgProvidersInScope[id]!
- ._createValue(context);
+ // The mock cannot be instantiated here, since no argument is available
+ // in this scope. It is only registered, so that the ProviderScopes
+ // inserting this argument provider into the widget tree can generate
+ // their intermediate providers out of the mock.
+ _overriddenArgProviders[id] = override._mockArgProvider;
}
}
@@ -427,101 +462,154 @@ class ProviderScopeState extends State {
@override
void dispose() {
- // dispose all the created providers
- createdProviderValues.forEach((key, value) {
- key._safeDisposeValue(value);
- });
+ _disposeCreatedValues();
- allArgProvidersInScope.clear();
- allProvidersInScope.clear();
- createdProviderValues.clear();
+ _allArgProvidersInScope.clear();
+ _allProvidersInScope.clear();
+ _overriddenProviders.clear();
+ _overriddenArgProviders.clear();
+ _createdValues.clear();
super.dispose();
}
- // Providers logic ----------------------------------------------------------
-
- /// Tries to find the intermediate [Provider] associated with this [id].
- Provider? getIntermediateProvider(Provider id) {
- return allProvidersInScope[id];
+ /// Disposes all the values created by this scope, by leveraging the
+ /// intermediate providers that created them.
+ ///
+ /// The values are disposed in the reverse order of creation. Since a provider
+ /// can inject the other providers of its own scope, and since every value is
+ /// created lazily, a value is always created *after* the values it depends on
+ /// ; therefore, reversing the creation order guarantees that a value is
+ /// always disposed *before* the values it depends on.
+ void _disposeCreatedValues() {
+ for (final entry in _createdValues.entries.toList().reversed) {
+ try {
+ entry.key._safeDisposeValue(entry.value);
+ } on Object catch (error, stackTrace) {
+ // A throwing dispose must not prevent the remaining values of this
+ // scope from being disposed, otherwise a single faulty dispose would
+ // leak everything else.
+ FlutterError.reportError(
+ FlutterErrorDetails(
+ exception: error,
+ stack: stackTrace,
+ library: 'disco',
+ context: ErrorDescription(
+ 'while disposing the value of ${entry.key._debugName}',
+ ),
+ ),
+ );
+ }
+ }
}
- /// Creates a provider value and stores it to [createdProviderValues].
- dynamic createProviderValue(Provider id, BuildContext context) {
- // find the intermediate provider in the list
- final provider = getIntermediateProvider(id)!;
+ /// Creates the value of [intermediateProvider], the intermediate provider
+ /// generated for the top-level provider [id], and stores it into
+ /// [_createdValues].
+ dynamic _createAndStoreValue(
+ Object id,
+ Provider intermediateProvider,
+ BuildContext context,
+ ) {
+ // A provider that is injected while its own value is being created can only
+ // be waiting for itself.
+ final cycleStart = _idsBeingCreated.indexOf(id);
+ if (cycleStart >= 0) {
+ throw ProviderCircularDependencyError([
+ ..._idsBeingCreated.skip(cycleStart),
+ id,
+ ]);
+ }
- // Temporarily override shared creation state
- final savedScope = _currentlyInitializingScope;
- final savedIndex = _currentlyCreatingProviderIndex;
- final savedProvider = _currentlyCreatingProvider;
+ _idsBeingCreated.add(id);
try {
- _currentlyInitializingScope = this;
- _currentlyCreatingProviderIndex = _providerIndices[id];
- _currentlyCreatingProvider = id;
-
- // Create the provider value (may throw or trigger nested creation)
- final value = provider._createValue(context);
- // Store the created provider value
- createdProviderValues[id] = value;
+ // Create the value (it may throw or trigger nested creations)
+ final value = intermediateProvider._createValue(context);
+ // Store the created value
+ _createdValues[intermediateProvider] = value;
return value;
} finally {
- // Restore shared state on both success and failure
- _currentlyInitializingScope = savedScope;
- _currentlyCreatingProviderIndex = savedIndex;
- _currentlyCreatingProvider = savedProvider;
+ _idsBeingCreated.removeLast();
}
}
+ // Providers logic ----------------------------------------------------------
+
+ /// Tries to find the intermediate [Provider] associated with this [id].
+ Provider? _getIntermediateProvider(Provider id) {
+ return _allProvidersInScope[id];
+ }
+
+ /// Tries to find the [Provider] overriding this [id].
+ ///
+ /// It returns null if this [id] is not overridden. Only the internal
+ /// [ProviderScope] of a [ProviderScopeOverride] can return a value here.
+ Provider? _getOverriddenProvider(Provider id) {
+ return _overriddenProviders[id];
+ }
+
+ /// Tries to find the value already created for this [id].
+ /// It returns null if the [id] is not in this scope or if its value has not
+ /// been created yet.
+ Object? _getCreatedProviderValue(Provider id) {
+ final provider = _getIntermediateProvider(id);
+ if (provider == null) return null;
+ return _createdValues[provider];
+ }
+
+ /// Creates a provider value and stores it to [_createdValues].
+ dynamic _createProviderValue(Provider id, BuildContext context) {
+ return _createAndStoreValue(id, _getIntermediateProvider(id)!, context);
+ }
+
/// Used to determine if the requested provider is present in the current
/// scope.
- bool isProviderInScope(Provider id) {
+ bool _isProviderInScope(Provider id) {
// Find the provider by type
- return getIntermediateProvider(id) != null;
+ return _getIntermediateProvider(id) != null;
}
// ArgProviders logic -------------------------------------------------------
/// Tries to find the intermediate [Provider] associated with this [id].
- Provider? getIntermediateProviderForArgProvider(
+ Provider? _getIntermediateProviderForArgProvider(
ArgProvider id,
) {
- return allArgProvidersInScope[id];
+ return _allArgProvidersInScope[id];
+ }
+
+ /// Tries to find the [ArgProvider] overriding this [id].
+ ///
+ /// It returns null if this [id] is not overridden. Only the internal
+ /// [ProviderScope] of a [ProviderScopeOverride] can return a value here.
+ ArgProvider? _getOverriddenArgProvider(ArgProvider id) {
+ return _overriddenArgProviders[id];
}
- /// Creates a provider value and stores it to [createdProviderValues].
- dynamic createProviderValueForArgProvider(
+ /// Tries to find the value already created for this [id].
+ /// It returns null if the [id] is not in this scope or if its value has not
+ /// been created yet.
+ Object? _getCreatedArgProviderValue(ArgProvider id) {
+ final provider = _getIntermediateProviderForArgProvider(id);
+ if (provider == null) return null;
+ return _createdValues[provider];
+ }
+
+ /// Creates a provider value and stores it to [_createdValues].
+ dynamic _createProviderValueForArgProvider(
ArgProvider id,
BuildContext context,
) {
- // find the intermediate provider in the list
- final provider = getIntermediateProviderForArgProvider(id)!;
-
- // Temporarily override shared creation state
- final savedScope = _currentlyInitializingScope;
- final savedIndex = _currentlyCreatingProviderIndex;
- final savedProvider = _currentlyCreatingProvider;
- try {
- _currentlyInitializingScope = this;
- _currentlyCreatingProviderIndex = _argProviderIndices[id];
- _currentlyCreatingProvider = id;
-
- // Create the provider value (may throw or trigger nested creation)
- final value = provider._createValue(context);
- // Store the created provider value
- createdProviderValues[allArgProvidersInScope[id]!] = value;
- return value;
- } finally {
- // Restore shared state on both success and failure
- _currentlyInitializingScope = savedScope;
- _currentlyCreatingProviderIndex = savedIndex;
- _currentlyCreatingProvider = savedProvider;
- }
+ return _createAndStoreValue(
+ id,
+ _getIntermediateProviderForArgProvider(id)!,
+ context,
+ );
}
/// Used to determine if the requested provider is present in the current
/// scope.
- bool isArgProviderInScope(ArgProvider id) {
- return getIntermediateProviderForArgProvider(id) != null;
+ bool _isArgProviderInScope(ArgProvider id) {
+ return _getIntermediateProviderForArgProvider(id) != null;
}
// Rest of _ProviderScopeState ----------------------------------------------
@@ -539,7 +627,7 @@ class ProviderScopeState extends State {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(
- IterableProperty('createdProviderValues', createdProviderValues.values),
+ IterableProperty('createdValues', _createdValues.values),
);
}
@@ -547,11 +635,14 @@ class ProviderScopeState extends State {
}
@immutable
-class _InheritedProvider extends InheritedModel {
+class _InheritedProvider extends InheritedWidget {
const _InheritedProvider({required this.state, required super.child});
final ProviderScopeState state;
+ /// The dependents of this widget are never notified: a [ProviderScope] hands
+ /// out values, and the values themselves never change. Reacting to a mutation
+ /// of a value is the job of the state management solution of choice.
// coverage:ignore-start
@override
bool updateShouldNotify(covariant _InheritedProvider oldWidget) {
@@ -559,85 +650,69 @@ class _InheritedProvider extends InheritedModel {
}
// coverage:ignore-end
- bool isSupportedAspectWithType(
- Provider? providerId,
- ArgProvider? argProviderId,
- ) {
+ /// Whether the scope of this widget provides the given ID.
+ ///
+ /// Exactly one of [providerId] and [argProviderId] must be given.
+ bool _provides(Provider? providerId, ArgProvider? argProviderId) {
assert(
(providerId != null) ^ (argProviderId != null),
'Either a Provider or an ArgProvider must be used as ID.',
);
if (providerId != null) {
- return state.isProviderInScope(providerId);
+ return state._isProviderInScope(providerId);
}
- return state.isArgProviderInScope(argProviderId!);
- }
-
- // coverage:ignore-start
- @override
- bool updateShouldNotifyDependent(
- covariant _InheritedProvider oldWidget,
- Set dependencies,
- ) {
- return false;
+ return state._isArgProviderInScope(argProviderId!);
}
- // coverage:ignore-end
- /// The following two methods are taken from [InheritedModel] and modified
- /// in order to find the first [_InheritedProvider] ancestor that contains
- /// the searched provider (aspect).
- /// This is a small optimization that avoids traversing all of the
- /// [ProviderScope] ancestors.
- static InheritedElement? _findNearestModel(
+ /// Returns the element of the nearest [_InheritedProvider] ancestor whose
+ /// scope provides the given ID, or null if there is none.
+ ///
+ /// This logic is adapted from [InheritedModel]: instead of stopping at the
+ /// nearest ancestor of this type, it keeps walking up until one of them
+ /// actually provides the ID. This is a small optimization that avoids
+ /// traversing every single element between two [ProviderScope]s.
+ static InheritedElement? _findNearestElementProviding(
BuildContext context,
Provider? providerId,
ArgProvider? argProviderId,
) {
- assert(
- (providerId != null) ^ (argProviderId != null),
- 'Either a Provider or an ArgProvider must be used as ID.',
- );
- final model = context
+ final element = context
.getElementForInheritedWidgetOfExactType<_InheritedProvider>();
// No ancestors of type _InheritedProvider found, exit.
- if (model == null) {
+ if (element == null) {
return null;
}
- assert(
- model.widget is _InheritedProvider,
- 'The widget must be of type _InheritedProvider',
- );
- final modelWidget = model.widget as _InheritedProvider;
+ final widget = element.widget as _InheritedProvider;
- // The model contains the aspect, the ancestor has been found, return it.
- if (modelWidget.isSupportedAspectWithType(providerId, argProviderId)) {
- return model;
+ // The ancestor providing the ID has been found, return it.
+ if (widget._provides(providerId, argProviderId)) {
+ return element;
}
- // The aspect has not been found in the current ancestor, go up to other
- // ancestors and try to find it.
- Element? modelParent;
- model.visitAncestorElements((Element ancestor) {
- modelParent = ancestor;
+ // This ancestor does not provide the ID: go further up and try again.
+ Element? parent;
+ element.visitAncestorElements((ancestor) {
+ parent = ancestor;
return false;
});
// Return null if we've reached the root.
- if (modelParent == null) {
+ if (parent == null) {
return null;
}
- return _findNearestModel(modelParent!, providerId, argProviderId);
+ return _findNearestElementProviding(parent!, providerId, argProviderId);
}
- /// Makes [context] dependent on the specified [providerId] of an
- /// [_InheritedProvider] (or [argProviderId], alternatively).
+ /// Returns the nearest [_InheritedProvider] ancestor whose scope provides the
+ /// given ID, or null if there is none.
///
- /// The dependencies created by this method target the nearest
- /// [_InheritedProvider] ancestor whose [isSupportedAspect] returns true.
+ /// Exactly one of [providerId] and [argProviderId] must be given.
///
- /// If no ancestor of type _InheritedProvider exists, null is returned.
- static _InheritedProvider? inheritFromNearest(
+ /// NB: no dependency is registered on the returned widget, since the values
+ /// of a [ProviderScope] never change and, thus, there would be nothing to
+ /// rebuild. The widget tree is merely walked.
+ static _InheritedProvider? findNearestProviding(
BuildContext context,
Provider? providerId,
ArgProvider? argProviderId,
@@ -647,17 +722,29 @@ class _InheritedProvider extends InheritedModel {
'Either a Provider or an ArgProvider must be used as ID.',
);
- // Try and find a model in the ancestors for which isSupportedAspect(aspect)
- // is true.
- final model = _findNearestModel(context, providerId, argProviderId);
- if (model == null) {
+ final element = _findNearestElementProviding(
+ context,
+ providerId,
+ argProviderId,
+ );
+ if (element == null) {
return null;
}
- return model.widget as _InheritedProvider;
+ return element.widget as _InheritedProvider;
}
}
+/// Returns a debug name for a top-level provider, which is either a [Provider]
+/// or an [ArgProvider].
+String _debugNameOf(Object provider) => switch (provider) {
+ final Provider p => p._debugName,
+ final ArgProvider ap => ap._debugName,
+ // coverage:ignore-start
+ _ => throw Exception('Unknown provider type ${provider.runtimeType}'),
+ // coverage:ignore-end
+};
+
/// {@template ProviderWithoutScopeError}
/// Error thrown when the [Provider] was never attached to a [ProviderScope].
/// {@endtemplate}
@@ -670,16 +757,8 @@ class ProviderWithoutScopeError extends Error {
@override
String toString() {
- final name = switch (provider) {
- final Provider p => p._debugName,
- final ArgProvider ap => ap._debugName,
- // coverage:ignore-start
- _ => throw Exception('Unknown provider type ${provider.runtimeType}'),
- // coverage:ignore-end
- };
-
- return 'Seems like that you forgot to provide the provider of type $name '
- 'to a ProviderScope.';
+ return 'Seems like that you forgot to provide the provider of type '
+ '${_debugNameOf(provider)} to a ProviderScope.';
}
}
@@ -711,52 +790,29 @@ class MultipleProviderOverrideOfSameInstance extends Error {
'same instance together.';
}
-/// {@template ProviderForwardReferenceError}
-/// Error thrown when a provider tries to access another provider that appears
-/// later in the same ProviderScope's providers list.
-///
-/// This prevents circular dependencies by enforcing that providers can only
-/// access providers defined earlier in the list.
+/// {@template ProviderCircularDependencyError}
+/// Error thrown when the value of a provider cannot be created because that
+/// provider directly or indirectly injects itself.
/// {@endtemplate}
-class ProviderForwardReferenceError extends Error {
- /// {@macro ProviderForwardReferenceError}
- ProviderForwardReferenceError({
- required this.currentProvider,
- required this.requestedProvider,
- });
-
- /// The provider currently being created
- final Object currentProvider;
+class ProviderCircularDependencyError extends Error {
+ /// {@macro ProviderCircularDependencyError}
+ ProviderCircularDependencyError(this.dependencyChain);
- /// The provider being requested
- final Object requestedProvider;
+ /// The providers taking part in the cycle, in the order in which their values
+ /// have been requested.
+ ///
+ /// The first and the last element are the same provider, i.e. the one closing
+ /// the cycle. Every element is either a [Provider] or an [ArgProvider].
+ final List dependencyChain;
@override
String toString() {
- final currentName = switch (currentProvider) {
- final Provider p => p._debugName,
- final ArgProvider ap => ap._debugName,
- // coverage:ignore-start
- _ => throw Exception(
- 'Unknown provider type ${currentProvider.runtimeType}',
- ),
- // coverage:ignore-end
- };
- final requestedName = switch (requestedProvider) {
- final Provider p => p._debugName,
- final ArgProvider ap => ap._debugName,
- // coverage:ignore-start
- _ => throw Exception(
- 'Unknown provider type ${requestedProvider.runtimeType}',
- ),
- // coverage:ignore-end
- };
-
- return 'Forward reference detected!\n\n'
- '`$currentName` tried to access `$requestedName`.\n\n'
- 'Providers in a ProviderScope can only access providers defined '
- 'EARLIER in the providers list. This prevents circular dependencies.\n'
- '\nTo fix: Move `$requestedName` before `$currentName` in your '
- 'providers list.';
+ final chain = dependencyChain.map(_debugNameOf).join('\n -> ');
+
+ return 'Circular dependency detected!\n\n'
+ ' $chain\n\n'
+ 'A provider cannot inject itself, not even indirectly. Break the cycle '
+ 'by making one of these providers independent of the others, or by '
+ 'injecting the value where it is used instead of where it is created.';
}
}
diff --git a/packages/disco/lib/src/widgets/provider_scope_override.dart b/packages/disco/lib/src/widgets/provider_scope_override.dart
index b1069d7d..08c62f41 100644
--- a/packages/disco/lib/src/widgets/provider_scope_override.dart
+++ b/packages/disco/lib/src/widgets/provider_scope_override.dart
@@ -55,7 +55,7 @@ class ProviderScopeOverrideState extends State {
final _providerScopeStateKey = GlobalKey();
/// The [ProviderScopeState] of the [ProviderScopeOverride] widget.
- ProviderScopeState get providerScopeState =>
+ ProviderScopeState get _providerScopeState =>
_providerScopeStateKey.currentState!;
@override
diff --git a/packages/disco/pubspec.yaml b/packages/disco/pubspec.yaml
index 3b90aa60..6133f99a 100644
--- a/packages/disco/pubspec.yaml
+++ b/packages/disco/pubspec.yaml
@@ -1,6 +1,6 @@
name: disco
description: A Flutter library bringing a new concept of scoped providers for dependency injection, which are independent of any specific state management solution.
-version: 2.0.0
+version: 3.0.0
repository: https://github.com/our-creativity/disco
homepage: https://disco.mariuti.com
documentation: https://disco.mariuti.com
@@ -11,6 +11,9 @@ topics:
environment:
sdk: ^3.10.0
+ # Flutter SDK constraint updated to a realistic lower bound.
+ # No APIs requiring Flutter 3.38.0 were found in the package.
+ flutter: ">=3.10.0"
resolution: workspace
diff --git a/packages/disco/test/disco_test.dart b/packages/disco/test/disco_test.dart
index 5b90b531..fcb16977 100644
--- a/packages/disco/test/disco_test.dart
+++ b/packages/disco/test/disco_test.dart
@@ -42,8 +42,8 @@ void main() {
home: Scaffold(
body: ProviderScope(
providers: [
- numberContainer1Provider,
- numberContainer2Provider,
+ numberContainer1Provider(),
+ numberContainer2Provider(),
],
child: Builder(
builder: (context) {
@@ -87,28 +87,58 @@ void main() {
expect(find.text('6'), findsOneWidget);
});
- testWidgets('Test Provider.withArgument not lazy', (tester) async {
+ testWidgets('The value of a provider is not created until it is injected', (
+ tester,
+ ) async {
var fired = false;
- final doubleCountProvider = Provider.withArgument(
- (context, int arg) {
- fired = true;
- return arg * 2;
- },
- lazy: false,
- );
+ final doubleCountProvider = Provider.withArgument((context, int arg) {
+ fired = true;
+ return arg * 2;
+ });
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
providers: [doubleCountProvider(3)],
+ // Nothing injects the provider.
child: const Text('hello'),
),
),
),
);
+ expect(fired, false);
+ });
+
+ testWidgets('''A value can be created as soon as its scope is mounted, by injecting it right below the scope''', (
+ tester,
+ ) async {
+ var fired = false;
+
+ final doubleCountProvider = Provider.withArgument((context, int arg) {
+ fired = true;
+ return arg * 2;
+ });
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ProviderScope(
+ providers: [doubleCountProvider(3)],
+ // This is the recommended way of creating a value eagerly.
+ child: Builder(
+ builder: (context) {
+ doubleCountProvider.of(context);
+ return const Text('hello');
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
expect(fired, true);
});
@@ -124,9 +154,9 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider],
+ providers: [numberProvider()],
child: ProviderScope(
- providers: [doubleNumberProvider],
+ providers: [doubleNumberProvider()],
child: Builder(
builder: (context) {
final number = numberProvider.of(context);
@@ -153,7 +183,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [zeroProvider],
+ providers: [zeroProvider()],
child: Builder(
builder: (context) {
final ten = tenProvider.of(context);
@@ -240,7 +270,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [nameContainerProvider],
+ providers: [nameContainerProvider()],
child: Builder(
builder: (context) {
final numberContainer = numberContainerProvider.maybeOf(
@@ -266,8 +296,8 @@ void main() {
home: Scaffold(
body: ProviderScope(
providers: [
- numberContainerProvider,
- numberContainerProvider,
+ numberContainerProvider(),
+ numberContainerProvider(),
],
child: const SizedBox(),
),
@@ -288,11 +318,9 @@ void main() {
final numberContainer1Provider = Provider(
(_) => const NumberContainer(1),
- lazy: false,
);
final numberContainer2Provider = Provider(
(_) => const NumberContainer(100),
- lazy: false,
);
final nameContainerProvider = Provider(
(_) => nameContainer,
@@ -310,9 +338,9 @@ void main() {
home: Scaffold(
body: ProviderScope(
providers: [
- nameContainerProvider,
- numberContainer1Provider,
- numberContainer2Provider,
+ nameContainerProvider(),
+ numberContainer1Provider(),
+ numberContainer2Provider(),
fullNameContainerProvider('Smith'),
],
child: Builder(
@@ -379,7 +407,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberContainerProvider],
+ providers: [numberContainerProvider()],
child: Builder(
builder: (context) {
return ElevatedButton(
@@ -435,7 +463,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [nameContainerProvider],
+ providers: [nameContainerProvider()],
child: Builder(
builder: (context) {
return ElevatedButton(
@@ -483,7 +511,7 @@ void main() {
mainContext: context,
child: ProviderScope(
providers: [
- secondNumberContainerProvider,
+ secondNumberContainerProvider(),
doubleCountProvider(3),
],
child: Builder(
@@ -509,7 +537,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberContainerProvider],
+ providers: [numberContainerProvider()],
child: Builder(
builder: (context) {
return ElevatedButton(
@@ -567,7 +595,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [baseProvider, doubleProvider],
+ providers: [baseProvider(), doubleProvider()],
child: Builder(
builder: (context) {
return ElevatedButton(
@@ -606,7 +634,7 @@ void main() {
builder: (BuildContext context, Key key, Widget? child) {
return ProviderScope(
key: key,
- providers: [numberProvider],
+ providers: [numberProvider()],
child: Builder(
builder: (context) {
final number = numberProvider.of(context);
@@ -702,15 +730,45 @@ void main() {
tester,
) async {
final numberProvider = Provider((_) => 0);
+ final mockNumberProvider = Provider((_) => 9);
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ final number = numberProvider.of(context);
+ return Text(number.toString());
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+ expect(find.text('9'), findsOneWidget);
+ });
+
+ testWidgets('''ProviderScopeOverride should override providers''', (
+ tester,
+ ) async {
+ final numberProvider = Provider((_) => 0);
+ final number100Provider = Provider((_) => 100);
+
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(100),
+ numberProvider.overrideWith(number100Provider),
],
child: MaterialApp(
home: ProviderScope(
providers: [
- numberProvider,
+ numberProvider(),
],
child: Builder(
builder: (context) {
@@ -729,10 +787,11 @@ void main() {
tester,
) async {
final numberProvider = Provider.withArgument((_, int arg) => arg);
+ final mockNumberProvider = Provider.withArgument((_, int arg) => 16);
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(16),
+ numberProvider.overrideWith(mockNumberProvider),
],
child: MaterialApp(
home: ProviderScope(
@@ -754,15 +813,18 @@ void main() {
testWidgets('Only one ProviderScopeOverride can be present', (tester) async {
final numberProvider = Provider((_) => 0);
+ final number100Provider = Provider((_) => 100);
+ final number200Provider = Provider((_) => 200);
+
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(100),
+ numberProvider.overrideWith(number100Provider),
],
child: MaterialApp(
home: ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(200),
+ numberProvider.overrideWith(number200Provider),
],
child: Builder(
builder: (context) {
@@ -836,12 +898,15 @@ void main() {
testWidgets(
'''ProviderScopeOverride must throw a MultipleProviderOverrideOfSameInstance for duplicated providers''',
(tester) async {
- final numberProvider = Provider((context) => 0);
+ final number0Provider = Provider((context) => 0);
+ final number1Provider = Provider((context) => 1);
+ final number2Provider = Provider((context) => 2);
+
await tester.pumpWidget(
ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(1),
- numberProvider.overrideWithValue(2),
+ number0Provider.overrideWith(number1Provider),
+ number0Provider.overrideWith(number2Provider),
],
child: const Text('hello'),
),
@@ -858,14 +923,20 @@ void main() {
'''Test ProviderScopeOverride throws MultipleProviderOverrideOfSameInstance for multiple instances of ArgProvider''',
(tester) async {
final numberProvider = Provider.withArgument((context, int arg) => arg);
+ final numberPlusOneProvider = Provider.withArgument(
+ (context, int arg) => arg + 1,
+ );
+ final numberPlusTwoProvider = Provider.withArgument(
+ (context, int arg) => arg + 2,
+ );
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScopeOverride(
overrides: [
- numberProvider.overrideWithValue(1),
- numberProvider.overrideWithValue(2),
+ numberProvider.overrideWith(numberPlusOneProvider),
+ numberProvider.overrideWith(numberPlusTwoProvider),
],
child: Builder(
builder: (context) {
@@ -885,20 +956,20 @@ void main() {
);
// Same-scope provider access tests
- testWidgets('Provider can access earlier provider in same scope (non-lazy)', (
+ testWidgets('Provider can access earlier provider in same scope', (
tester,
) async {
- final numberProvider = Provider((_) => 5, lazy: false);
+ final numberProvider = Provider((_) => 5);
final doubleProvider = Provider((context) {
final number = numberProvider.of(context);
return number * 2;
- }, lazy: false);
+ });
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider, doubleProvider],
+ providers: [numberProvider(), doubleProvider()],
child: Builder(
builder: (context) {
final double = doubleProvider.of(context);
@@ -913,37 +984,11 @@ void main() {
expect(find.text('10'), findsOneWidget);
});
- testWidgets('Throws ProviderForwardReferenceError on forward reference', (
+ testWidgets('''The declaration order within a scope does not matter''', (
tester,
) async {
- final numberProvider = Provider((_) => 5, lazy: false);
+ final numberProvider = Provider((_) => 5);
final doubleProvider = Provider((context) {
- final number = numberProvider.of(context); // Forward reference!
- return number * 2;
- }, lazy: false);
-
- await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: ProviderScope(
- // Wrong order: doubleProvider depends on numberProvider
- // but comes first
- providers: [doubleProvider, numberProvider],
- child: Container(),
- ),
- ),
- ),
- );
-
- expect(
- tester.takeException(),
- const TypeMatcher(),
- );
- });
-
- testWidgets('Lazy provider can access earlier lazy provider', (tester) async {
- final numberProvider = Provider((_) => 5);
- final doubleProvider = Provider((context) {
final number = numberProvider.of(context);
return number * 2;
});
@@ -952,11 +997,14 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider, doubleProvider],
+ // `doubleProvider` depends on `numberProvider`, but it is declared
+ // first. Since every value is created lazily, and since all the
+ // providers of a scope are registered before any value exists, the
+ // dependency is resolved just fine.
+ providers: [doubleProvider(), numberProvider()],
child: Builder(
builder: (context) {
- final double = doubleProvider.of(context);
- return Text(double.toString());
+ return Text(doubleProvider.of(context).toString());
},
),
),
@@ -967,52 +1015,22 @@ void main() {
expect(find.text('10'), findsOneWidget);
});
- testWidgets('Non-lazy provider can access lazy earlier provider', (
+ testWidgets('ArgProvider can access another provider of the same scope', (
tester,
) async {
final numberProvider = Provider((_) => 5);
- final doubleProvider = Provider((context) {
- final number = numberProvider.of(context);
- return number * 2;
- }, lazy: false); // non-lazy
-
- await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: ProviderScope(
- providers: [numberProvider, doubleProvider],
- child: Builder(
- builder: (context) {
- final double = doubleProvider.of(context);
- return Text(double.toString());
- },
- ),
- ),
- ),
- ),
- );
-
- // doubleProvider's creation should trigger numberProvider's creation
- expect(find.text('10'), findsOneWidget);
- });
-
- testWidgets('ArgProvider can access earlier provider in same scope', (
- tester,
- ) async {
- final numberProvider = Provider((_) => 5, lazy: false);
final multiplierProvider = Provider.withArgument(
(context, int multiplier) {
final number = numberProvider.of(context);
return number * multiplier;
},
- lazy: false,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [numberProvider, multiplierProvider(3)],
+ providers: [numberProvider(), multiplierProvider(3)],
child: Builder(
builder: (context) {
final result = multiplierProvider.of(context);
@@ -1028,21 +1046,19 @@ void main() {
});
testWidgets('Nested provider dependencies work (A→B→C)', (tester) async {
- final aProvider = Provider((_) => 1, lazy: false);
+ final aProvider = Provider((_) => 1);
final bProvider = Provider(
(context) => aProvider.of(context) + 1,
- lazy: false,
);
final cProvider = Provider(
(context) => bProvider.of(context) + 1,
- lazy: false,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [aProvider, bProvider, cProvider],
+ providers: [aProvider(), bProvider(), cProvider()],
child: Builder(
builder: (context) {
final c = cProvider.of(context);
@@ -1080,7 +1096,7 @@ void main() {
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [aProvider, bProvider, cProvider],
+ providers: [aProvider(), bProvider(), cProvider()],
child: Builder(
builder: (context) {
// Access B first, which will create A
@@ -1101,28 +1117,29 @@ void main() {
expect(creationCount, 1);
});
- testWidgets('Mixed Provider and ArgProvider respect order', (tester) async {
- final numberProvider = Provider((_) => 5, lazy: false);
+ testWidgets('Mixed Provider and ArgProvider dependencies work', (
+ tester,
+ ) async {
+ final numberProvider = Provider((_) => 5);
final argProvider = Provider.withArgument(
(context, String prefix) {
final number = numberProvider.of(context);
return '$prefix$number';
},
- lazy: false,
);
final combineProvider = Provider((context) {
final str = argProvider.of(context);
return '$str!';
- }, lazy: false);
+ });
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
providers: [
- numberProvider, // index 0
- argProvider('num:'), // index 1
- combineProvider, // index 2
+ numberProvider(),
+ argProvider('num:'),
+ combineProvider(),
],
child: Builder(
builder: (context) {
@@ -1138,36 +1155,33 @@ void main() {
expect(find.text('num:5!'), findsOneWidget);
});
- testWidgets('Throws ProviderForwardReferenceError when ArgProvider '
- 'accesses later Provider', (tester) async {
- final numberProvider = Provider((_) => 5, lazy: false);
+ testWidgets('''An ArgProvider can access a Provider declared later''', (
+ tester,
+ ) async {
+ final numberProvider = Provider((_) => 5);
final multiplierProvider = Provider.withArgument(
(context, int multiplier) {
- final number = numberProvider.of(context); // Forward reference!
+ final number = numberProvider.of(context);
return number * multiplier;
},
- lazy: false,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- // Wrong order: multiplierProvider depends on numberProvider
- // but comes first
- providers: [multiplierProvider(3), numberProvider],
- child: Container(),
+ providers: [multiplierProvider(3), numberProvider()],
+ child: Builder(
+ builder: (context) {
+ return Text(multiplierProvider.of(context).toString());
+ },
+ ),
),
),
),
);
- // ArgProvider accessing a regular Provider throws
- // ProviderForwardReferenceError
- expect(
- tester.takeException(),
- const TypeMatcher(),
- );
+ expect(find.text('15'), findsOneWidget);
});
testWidgets('Multiple lazy ArgProviders accessing same earlier provider '
@@ -1219,123 +1233,912 @@ void main() {
expect(creationCount, 1);
});
- testWidgets('Throws ProviderForwardReferenceError when accessing '
- 'later ArgProvider', (tester) async {
+ testWidgets('''An ArgProvider can access an ArgProvider declared later''', (
+ tester,
+ ) async {
final secondArgProvider = Provider.withArgument(
(context, int arg) => arg * 2,
- lazy: false,
);
final firstArgProvider = Provider.withArgument(
(context, int arg) {
- final second = secondArgProvider.of(context); // Forward reference!
+ final second = secondArgProvider.of(context);
return arg + second;
},
- lazy: false,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- // Wrong order: firstArgProvider depends on secondArgProvider
- // but comes first
providers: [firstArgProvider(5), secondArgProvider(3)],
- child: Container(),
+ child: Builder(
+ builder: (context) {
+ return Text(firstArgProvider.of(context).toString());
+ },
+ ),
),
),
),
);
- expect(
- tester.takeException(),
- const TypeMatcher(),
- );
+ // 5 + (3 * 2)
+ expect(find.text('11'), findsOneWidget);
});
// Circular dependency tests
group('Circular dependency prevention', () {
+ testWidgets('''Direct circular dependency in the same scope (A→B, B→A)''', (
+ tester,
+ ) async {
+ late final Provider providerA;
+ late final Provider providerB;
+
+ providerA = Provider((context) {
+ return providerB.of(context) + 1;
+ }, debugName: 'A');
+
+ providerB = Provider((context) {
+ return providerA.of(context) + 1;
+ }, debugName: 'B');
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ProviderScope(
+ providers: [providerA(), providerB()],
+ child: Builder(
+ builder: (context) {
+ providerA.of(context);
+ return Container();
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final error = tester.takeException();
+ expect(error, isA());
+ final cycleError = error as ProviderCircularDependencyError;
+ // The chain reports the cycle, i.e. A -> B -> A.
+ expect(cycleError.dependencyChain, [providerA, providerB, providerA]);
+ expect(
+ cycleError.toString(),
+ contains('Circular dependency detected!'),
+ );
+ });
+
+ testWidgets('''Circular dependency of a provider with itself (A→A)''', (
+ tester,
+ ) async {
+ late final Provider providerA;
+ providerA = Provider((context) => providerA.of(context) + 1);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ProviderScope(
+ providers: [providerA()],
+ child: Builder(
+ builder: (context) {
+ providerA.of(context);
+ return Container();
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(
+ tester.takeException(),
+ isA(),
+ );
+ });
+
+ testWidgets('''Circular dependency mixing an ArgProvider and a Provider''', (
+ tester,
+ ) async {
+ late final Provider providerA;
+ late final ArgProvider argProviderB;
+
+ providerA = Provider((context) {
+ return argProviderB.of(context) + 1;
+ });
+
+ argProviderB = Provider.withArgument((context, arg) {
+ return providerA.of(context) + 1;
+ });
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ProviderScope(
+ providers: [providerA(), argProviderB('test')],
+ child: Builder(
+ builder: (context) {
+ providerA.of(context);
+ return Container();
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ final error = tester.takeException();
+ expect(error, isA());
+ expect(
+ (error as ProviderCircularDependencyError).dependencyChain,
+ [providerA, argProviderB, providerA],
+ );
+ });
+
+ testWidgets('''Circular dependency spanning two nested scopes''', (
+ tester,
+ ) async {
+ late final Provider providerA;
+ late final Provider providerB;
+
+ providerA = Provider((context) => providerB.of(context) + 1);
+ providerB = Provider((context) => providerA.of(context) + 1);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Scaffold(
+ body: ProviderScope(
+ providers: [providerA()],
+ child: ProviderScope(
+ providers: [providerB()],
+ child: Builder(
+ builder: (context) {
+ providerA.of(context);
+ return Container();
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(
+ tester.takeException(),
+ isA(),
+ );
+ });
+
testWidgets(
- 'Impossible: Direct circular dependency in same scope (A→B, B→A)',
+ '''The same provider can be created again after a failed creation''',
(tester) async {
- // This test demonstrates that circular dependencies are impossible
- // within the same scope due to forward reference errors.
- // Provider A tries to access Provider B, which comes later in the list,
- // resulting in a forward reference error.
- late final Provider providerA;
- late final Provider providerB;
-
- providerA = Provider((context) {
- final b = providerB.of(context); // Forward reference to B!
- return b + 1;
- }, lazy: false);
-
- providerB = Provider((context) {
- // In a circular dependency, B would try to access A, but A comes
- // first so this wouldn't be a forward reference. However, A accessing
- // B is already a forward reference, so we never get here.
- return 10;
- }, lazy: false);
+ // Regression test: the bookkeeping of the providers being created must
+ // be restored even when the creation throws.
+ var attempts = 0;
+ final throwingProvider = Provider((_) {
+ attempts++;
+ if (attempts == 1) throw StateError('boom');
+ return 42;
+ });
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProviderScope(
- providers: [providerA, providerB],
- child: Container(),
+ providers: [throwingProvider()],
+ child: Builder(
+ builder: (context) {
+ int? value;
+ try {
+ value = throwingProvider.of(context);
+ // ignore: avoid_catching_errors
+ } on StateError {
+ value = throwingProvider.of(context);
+ }
+ return Text(value.toString());
+ },
+ ),
),
),
),
);
- // A accessing B (which comes later) throws
- // ProviderForwardReferenceError
- expect(
- tester.takeException(),
- const TypeMatcher(),
- );
+ expect(find.text('42'), findsOneWidget);
+ expect(attempts, 2);
},
);
+ });
+ // Lifecycle and precedence of the overrides
+ group('Overrides', () {
testWidgets(
- 'Impossible: ArgProvider circular dependency with regular Provider',
+ '''The dispose of an overridden provider is the one of the mock''',
(tester) async {
- // This test demonstrates that circular dependencies are also impossible
- // when mixing ArgProvider and regular Provider.
- late final Provider providerA;
- late final ArgProvider argProviderB;
-
- providerA = Provider((context) {
- final b = argProviderB.of(context); // Forward reference!
- return b + 1;
- }, lazy: false);
-
- argProviderB = Provider.withArgument(
- (context, String arg) {
- // In a circular dependency, B would try to access A
- // But we never get here because A accessing B is already
- // a forward reference error.
- return 10;
+ var originalDisposed = false;
+ var mockDisposed = false;
+
+ final numberProvider = Provider(
+ (_) => 0,
+ dispose: (_) {
+ originalDisposed = true;
+ },
+ );
+ final mockNumberProvider = Provider(
+ (_) => 9,
+ dispose: (_) {
+ mockDisposed = true;
},
- lazy: false,
);
await tester.pumpWidget(
- MaterialApp(
- home: Scaffold(
- body: ProviderScope(
- providers: [providerA, argProviderB('test')],
- child: Container(),
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
),
),
),
);
- // A accessing B (which comes later) throws
- // ProviderForwardReferenceError
- expect(
- tester.takeException(),
- const TypeMatcher(),
+ expect(find.text('9'), findsOneWidget);
+ expect(mockDisposed, false);
+
+ // Dispose the whole tree
+ await tester.pumpWidget(Container());
+
+ expect(mockDisposed, true);
+ // The value of the original provider has never been created, therefore
+ // its dispose is never called.
+ expect(originalDisposed, false);
+ },
+ );
+
+ testWidgets(
+ '''An overridden provider is disposed by the ProviderScope providing it''',
+ (tester) async {
+ final disposedMockValues = [];
+ var created = 0;
+
+ final numberProvider = Provider((_) => 0);
+ final mockNumberProvider = Provider(
+ (_) => ++created,
+ dispose: disposedMockValues.add,
+ );
+
+ Widget buildTree({required bool showScope}) {
+ return ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: showScope
+ ? ProviderScope(
+ providers: [numberProvider()],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ )
+ : const SizedBox.shrink(),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildTree(showScope: true));
+ expect(find.text('1'), findsOneWidget);
+ expect(disposedMockValues, isEmpty);
+
+ // Only the ProviderScope providing the overridden provider is disposed:
+ // the mock has to be disposed with it, exactly like the original
+ // provider would have been.
+ await tester.pumpWidget(buildTree(showScope: false));
+ expect(disposedMockValues, [1]);
+
+ // Bringing the ProviderScope back creates a brand new mock value.
+ await tester.pumpWidget(buildTree(showScope: true));
+ expect(find.text('2'), findsOneWidget);
+
+ await tester.pumpWidget(Container());
+ expect(disposedMockValues, [1, 2]);
+ },
+ );
+
+ testWidgets(
+ '''A disposed ProviderScopeOverride recreates the values it owns''',
+ (tester) async {
+ final disposedMockValues = [];
+ var created = 0;
+
+ final numberProvider = Provider((_) => 0);
+ final mockNumberProvider = Provider(
+ (_) => ++created,
+ dispose: disposedMockValues.add,
);
+
+ Widget buildTree(Key key) {
+ return ProviderScopeOverride(
+ key: key,
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ // No ProviderScope provides `numberProvider` here, therefore the
+ // value of the mock lives in the internal ProviderScope of the
+ // ProviderScopeOverride itself.
+ child: MaterialApp(
+ home: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ ),
+ );
+ }
+
+ await tester.pumpWidget(buildTree(const ValueKey('first')));
+ expect(find.text('1'), findsOneWidget);
+ expect(disposedMockValues, isEmpty);
+
+ // Changing the key forces the ProviderScopeOverride to be disposed and
+ // built again from scratch: its value has to be disposed and recreated
+ // with it.
+ await tester.pumpWidget(buildTree(const ValueKey('second')));
+ expect(disposedMockValues, [1]);
+ expect(find.text('2'), findsOneWidget);
+
+ await tester.pumpWidget(Container());
+ expect(disposedMockValues, [1, 2]);
+ },
+ );
+
+ testWidgets(
+ '''Two ProviderScopes providing an overridden provider get two values''',
+ (tester) async {
+ var created = 0;
+ final numberProvider = Provider((_) => 0);
+ final mockNumberProvider = Provider((_) => ++created);
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: Column(
+ children: [
+ for (var i = 0; i < 2; i++)
+ ProviderScope(
+ providers: [numberProvider()],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ // Exactly like the original provider, the mock creates one value per
+ // ProviderScope providing it.
+ expect(find.text('1'), findsOneWidget);
+ expect(find.text('2'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ '''The same mock can override two providers without sharing its value''',
+ (tester) async {
+ var created = 0;
+ final firstProvider = Provider((_) => 0);
+ final secondProvider = Provider((_) => 0);
+ // The very same mock instance is used for both overrides.
+ final mockProvider = Provider((_) => ++created);
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ firstProvider.overrideWith(mockProvider),
+ secondProvider.overrideWith(mockProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [firstProvider(), secondProvider()],
+ child: Builder(
+ builder: (context) {
+ final first = firstProvider.of(context);
+ final second = secondProvider.of(context);
+ return Text('$first $second');
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // The two overridden providers keep being independent from each other.
+ expect(find.text('1 2'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ '''An overridden argument provider is disposed by the ProviderScope providing it''',
+ (tester) async {
+ var originalDisposed = false;
+ final disposedMockValues = [];
+
+ final numberProvider = Provider.withArgument(
+ (_, arg) => arg,
+ dispose: (_) {
+ originalDisposed = true;
+ },
+ );
+ final mockNumberProvider = Provider.withArgument(
+ (_, arg) => arg + 100,
+ dispose: disposedMockValues.add,
+ );
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(1),
+ ],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // The mock receives the argument specified in the widget tree.
+ expect(find.text('101'), findsOneWidget);
+ expect(disposedMockValues, isEmpty);
+
+ // Dispose the whole tree
+ await tester.pumpWidget(Container());
+
+ expect(disposedMockValues, [101]);
+ expect(originalDisposed, false);
+ },
+ );
+
+ testWidgets(
+ '''The value of an overridden provider is never created''',
+ (tester) async {
+ var originalCreated = false;
+
+ final numberProvider = Provider((_) {
+ originalCreated = true;
+ return 1;
+ });
+ final mockNumberProvider = Provider((_) => 10);
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.text('10'), findsOneWidget);
+ // Since every value is created lazily, and every injection resolves to
+ // the mock, the original provider is never created at all.
+ expect(originalCreated, false);
+ },
+ );
+
+ testWidgets(
+ '''A mock can inject the other providers of the widget tree''',
+ (tester) async {
+ final numberProvider = Provider((_) => 1);
+ final baseProvider = Provider((_) => 100);
+ // The mock is created lazily, therefore it can inject the providers
+ // available where the overridden provider is injected.
+ final mockNumberProvider = Provider(
+ (context) => baseProvider.of(context) + 5,
+ );
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ baseProvider(),
+ numberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ return Text(numberProvider.of(context).toString());
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.text('105'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ '''An argument provider override does not affect the ProviderScopes above the ProviderScopeOverride''',
+ (tester) async {
+ final numberProvider = Provider((_) => 0);
+ final mockNumberProvider = Provider((_) => 9);
+ final numberArgProvider = Provider.withArgument(
+ (_, arg) => arg,
+ );
+ final mockNumberArgProvider = Provider.withArgument(
+ (_, arg) => arg + 100,
+ );
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ numberArgProvider(1),
+ ],
+ child: ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ numberArgProvider.overrideWith(mockNumberArgProvider),
+ ],
+ child: Builder(
+ builder: (context) {
+ final number = numberProvider.of(context);
+ final numberArg = numberArgProvider.of(context);
+ return Text('$number $numberArg');
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // The provider without argument is overridden, because the internal
+ // ProviderScope of the ProviderScopeOverride provides the mock itself
+ // and, being nearer to the injecting widget, it shadows the ancestor
+ // scope.
+ //
+ // The argument provider is NOT overridden, since its intermediate
+ // provider can only be generated by the ProviderScope providing it (the
+ // only place where the argument is known), and that scope is an
+ // ancestor of the ProviderScopeOverride, i.e. it cannot know about the
+ // overrides.
+ expect(find.text('9 1'), findsOneWidget);
+ },
+ );
+ });
+
+ // Regression tests: when a provider is overridden, the providers *depending*
+ // on it get the override as well, no matter which scope declares them.
+ group('Overrides of dependencies', () {
+ testWidgets(
+ '''A provider injecting an overridden provider of the same scope gets the override''',
+ (tester) async {
+ final numberProvider = Provider(
+ (_) => 1,
+ debugName: 'number',
+ );
+ final doubleNumberProvider = Provider(
+ (context) => numberProvider.of(context) * 2,
+ debugName: 'doubleNumber',
+ );
+ final mockNumberProvider = Provider((_) => 10);
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ doubleNumberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ final number = numberProvider.of(context);
+ final doubleNumber = doubleNumberProvider.of(context);
+ return Text('$number $doubleNumber');
+ },
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // The widget gets the override (10), therefore the provider depending
+ // on it gets 20 and not 2, i.e. it is created out of the override and
+ // not out of the original provider.
+ expect(find.text('10 20'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ '''A provider injecting an overridden provider of an ANCESTOR scope gets the override''',
+ (tester) async {
+ final numberProvider = Provider(
+ (_) => 1,
+ debugName: 'number',
+ );
+ final doubleNumberProvider = Provider(
+ (context) => numberProvider.of(context) * 2,
+ debugName: 'doubleNumber',
+ );
+ final mockNumberProvider = Provider((_) => 10);
+
+ await tester.pumpWidget(
+ ProviderScopeOverride(
+ overrides: [
+ numberProvider.overrideWith(mockNumberProvider),
+ ],
+ child: MaterialApp(
+ home: ProviderScope(
+ providers: [
+ numberProvider(),
+ ],
+ // The dependent provider is provided by another scope,
+ // therefore the dependency is not resolved within the scope
+ // creating it.
+ child: ProviderScope(
+ providers: [
+ doubleNumberProvider(),
+ ],
+ child: Builder(
+ builder: (context) {
+ final number = numberProvider.of(context);
+ final doubleNumber = doubleNumberProvider.of(context);
+ return Text('$number $doubleNumber');
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // The dependency belongs to another scope, which has always been
+ // resolved through the widget tree.
+ expect(find.text('10 20'), findsOneWidget);
+ },
+ );
+ });
+
+ group('Disposal', () {
+ testWidgets(
+ '''The values are disposed in the reverse order of creation''',
+ (tester) async {
+ final disposals = [];
+
+ final aProvider = Provider(
+ (_) => 'a',
+ dispose: disposals.add,
+ );
+ // `b` depends on `a`, therefore the value of `a` is created first and
+ // has to be disposed last.
+ final bProvider = Provider(
+ (context) => '${aProvider.of(context)}b',
+ dispose: disposals.add,
+ );
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: ProviderScope(
+ // The declaration order is deliberately the opposite of the
+ // creation order, to make sure the latter is what counts.
+ providers: [bProvider(), aProvider()],
+ child: Builder(
+ builder: (context) => Text(bProvider.of(context)),
+ ),
+ ),
+ ),
+ );
+
+ expect(find.text('ab'), findsOneWidget);
+ expect(disposals, isEmpty);
+
+ await tester.pumpWidget(Container());
+
+ // `ab` depends on `a`, therefore it is disposed first.
+ expect(disposals, ['ab', 'a']);
+ },
+ );
+
+ testWidgets(
+ '''The reverse order of creation also holds across the two kinds of providers''',
+ (tester) async {
+ final disposals =