From 4c95560bbc0ea511586a2a2cff8feb82fa5c18ac Mon Sep 17 00:00:00 2001 From: Manuel Plavsic <55398763+manuel-plavsic@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:21:33 +0000 Subject: [PATCH 1/7] Add option to override whole provider/argprovider --- .../overrides/provider_argument_override.dart | 63 +++++++++++++++---- .../models/overrides/provider_override.dart | 51 ++++++++++++--- .../lib/src/models/providers/provider.dart | 13 +++- .../models/providers/provider_argument.dart | 12 +++- .../disco/lib/src/widgets/provider_scope.dart | 9 +-- packages/disco/test/disco_test.dart | 38 ++++++++++- 6 files changed, 153 insertions(+), 33 deletions(-) diff --git a/packages/disco/lib/src/models/overrides/provider_argument_override.dart b/packages/disco/lib/src/models/overrides/provider_argument_override.dart index 77dfbca..7300e58 100644 --- a/packages/disco/lib/src/models/overrides/provider_argument_override.dart +++ b/packages/disco/lib/src/models/overrides/provider_argument_override.dart @@ -1,28 +1,67 @@ part of '../../disco_internal.dart'; +sealed class _ArgProviderOverrideType {} + +// TODO (manuel-plavsic): I would get rid of this (and remove sealed class) +final class _ArgProviderOverrideWithValue + extends _ArgProviderOverrideType { + _ArgProviderOverrideWithValue._(this.argument, this.value, this.debugName); + + final A argument; + + final T value; + + /// {@macro Provider.debugName} + final String? debugName; +} + +final class _ArgProviderOverrideWithProvider + extends _ArgProviderOverrideType { + _ArgProviderOverrideWithProvider._(this.mockProvider); + final ArgProvider mockProvider; +} + /// Override that, if inserted into the widget tree, takes precedence over -/// [_argProvider]. +/// [_originalArgProvider]. @immutable class ArgProviderOverride extends Override { - ArgProviderOverride._(this._argProvider, T value, {this.debugName}) - : _value = value, + ArgProviderOverride._withValue( + this._originalArgProvider, + A arg, + T value, + String? debugName, + ) : _overrideType = _ArgProviderOverrideWithValue._( + arg, + value, + debugName, + ), + super._(); + + ArgProviderOverride._withArgProvider( + this._originalArgProvider, + ArgProvider mockProvider, + ) : _overrideType = _ArgProviderOverrideWithProvider._(mockProvider), super._(); /// The reference of the argument provider to override. - final ArgProvider _argProvider; + final ArgProvider _originalArgProvider; /// The overridden value. - final T _value; + final _ArgProviderOverrideType _overrideType; // 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; + Provider _generateIntermediateProvider() => switch (_overrideType) { + // TODO (manuel-plavsic): I would get rid of this + _ArgProviderOverrideWithValue(:final T value) => Provider( + (_) => value, + lazy: false, + ), + // TODO (manuel-plavsic): I would keep only this part below (and remove pattern matching) + // TODO (manuel-plavsic): this requires a major change in ProviderScope: a new layer of intermediate providers should be added (this time of type ArgProvider, not just Provider) + _ArgProviderOverrideWithProvider(:final ArgProvider mockProvider) => + mockProvider, + }; } diff --git a/packages/disco/lib/src/models/overrides/provider_override.dart b/packages/disco/lib/src/models/overrides/provider_override.dart index 166f908..c05ee0d 100644 --- a/packages/disco/lib/src/models/overrides/provider_override.dart +++ b/packages/disco/lib/src/models/overrides/provider_override.dart @@ -1,26 +1,57 @@ part of '../../disco_internal.dart'; +sealed class _ProviderOverrideType {} + +// TODO (manuel-plavsic): I would get rid of this (and remove sealed class) +final class _ProviderOverrideWithValue + extends _ProviderOverrideType { + _ProviderOverrideWithValue._(this.value, this.debugName); + final T value; + + /// {@macro Provider.debugName} + final String? debugName; +} + +final class _ProviderOverrideWithProvider + extends _ProviderOverrideType { + _ProviderOverrideWithProvider._(this.mockProvider); + final Provider mockProvider; +} + /// Override that, if inserted into the widget tree, takes precedence over -/// [_provider]. +/// [_originalProvider]. @immutable class ProviderOverride extends Override { - ProviderOverride._( - this._provider, + ProviderOverride._withValue( + this._originalProvider, T value, - ) : _value = value, + String? debugName, + ) : _overrideType = _ProviderOverrideWithValue._(value, debugName), + super._(); + + ProviderOverride._withProvider( + this._originalProvider, + Provider mockProvider, + ) : _overrideType = _ProviderOverrideWithProvider._(mockProvider), super._(); /// The reference of the provider to override. - final Provider _provider; + final Provider _originalProvider; - final T _value; + final _ProviderOverrideType _overrideType; // Utils leveraged by ProviderScope ----------------------------------------- /// Creates a [Provider]. /// This method is used internally by [ProviderScope]. - Provider _generateIntermediateProvider() => Provider( - (_) => _value, - lazy: false, - ); + Provider _generateIntermediateProvider() => switch (_overrideType) { + // TODO (manuel-plavsic): I would get rid of this + _ProviderOverrideWithValue(:final T value) => Provider( + (_) => value, + lazy: false, + ), + // TODO (manuel-plavsic): I would keep only this (and remove pattern matching) + _ProviderOverrideWithProvider(:final Provider mockProvider) => + mockProvider, + }; } diff --git a/packages/disco/lib/src/models/providers/provider.dart b/packages/disco/lib/src/models/providers/provider.dart index c1ffeaa..6af6b76 100644 --- a/packages/disco/lib/src/models/providers/provider.dart +++ b/packages/disco/lib/src/models/providers/provider.dart @@ -81,8 +81,17 @@ class Provider extends InstantiableProvider { /// [ProviderScopeOverride]. /// {@endtemplate} @visibleForTesting - ProviderOverride overrideWithValue(T value) => - ProviderOverride._(this, value); + ProviderOverride overrideWithValue(T value, {String? debugName}) => + ProviderOverride._withValue(this, value, debugName); + + /// {@template Provider.overrideWithValue} + /// It creates an override of this provider to be passed to + /// [ProviderScopeOverride]. + /// {@endtemplate} + @visibleForTesting + ProviderOverride overrideWithProvider( + Provider override, + ) => ProviderOverride._withProvider(this, override); // DI methods --------------------------------------------------------------- diff --git a/packages/disco/lib/src/models/providers/provider_argument.dart b/packages/disco/lib/src/models/providers/provider_argument.dart index c64d52e..4e54d72 100644 --- a/packages/disco/lib/src/models/providers/provider_argument.dart +++ b/packages/disco/lib/src/models/providers/provider_argument.dart @@ -38,8 +38,16 @@ class ArgProvider { /// {@macro Provider.overrideWithValue} @visibleForTesting - ArgProviderOverride overrideWithValue(T value) => - ArgProviderOverride._(this, value, debugName: debugName); + ArgProviderOverride overrideWithValue( + A arg, + T value, { + String? debugName, + }) => ArgProviderOverride._withValue(this, arg, value, debugName); + + @visibleForTesting + ArgProviderOverride overrideWithProvider( + ArgProvider override, + ) => ArgProviderOverride._withArgProvider(this, override); // --- // DI methods diff --git a/packages/disco/lib/src/widgets/provider_scope.dart b/packages/disco/lib/src/widgets/provider_scope.dart index 287409d..f219a4d 100644 --- a/packages/disco/lib/src/widgets/provider_scope.dart +++ b/packages/disco/lib/src/widgets/provider_scope.dart @@ -355,7 +355,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,7 +367,7 @@ class ProviderScopeState extends State { ); for (final override in providerOverrides) { - final id = override._provider; + final id = override._originalProvider; allProvidersInScope[id] = override._generateIntermediateProvider(); @@ -388,7 +388,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,7 +401,7 @@ class ProviderScopeState extends State { ); for (final override in argProviderOverrides) { - final id = override._argProvider; + final id = override._originalArgProvider; allArgProvidersInScope[id] = override._generateIntermediateProvider(); diff --git a/packages/disco/test/disco_test.dart b/packages/disco/test/disco_test.dart index 5b90b53..631c0b7 100644 --- a/packages/disco/test/disco_test.dart +++ b/packages/disco/test/disco_test.dart @@ -698,6 +698,36 @@ void main() { expect(textFinder('number: 1'), findsOneWidget); }); + testWidgets('''ProviderScopeOverride should override providers''', ( + tester, + ) async { + final numberProvider = Provider((_) => 0); + final mockNumberProvider = Provider((_) => 9); + await tester.pumpWidget( + ProviderScopeOverride( + overrides: [ + numberProvider.overrideWithProvider(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); + }); + + // TODO(manuel): add other test (check also that debug is different, etc.) + testWidgets('''ProviderScopeOverride should override providers''', ( tester, ) async { @@ -729,10 +759,12 @@ void main() { tester, ) async { final numberProvider = Provider.withArgument((_, int arg) => arg); + final mockNumberProvider = Provider.withArgument((_, int arg) => 4); await tester.pumpWidget( ProviderScopeOverride( overrides: [ - numberProvider.overrideWithValue(16), + // numberProvider.overrideWithValue(1, 16), + numberProvider.overrideWithProvider(mockNumberProvider), ], child: MaterialApp( home: ProviderScope( @@ -864,8 +896,8 @@ void main() { home: Scaffold( body: ProviderScopeOverride( overrides: [ - numberProvider.overrideWithValue(1), - numberProvider.overrideWithValue(2), + numberProvider.overrideWithValue(0, 1), + numberProvider.overrideWithValue(1, 2), ], child: Builder( builder: (context) { From 097b4a1b2b24458e914c2de8b3bc096ede0e1be2 Mon Sep 17 00:00:00 2001 From: Manuel Plavsic <55398763+manuel-plavsic@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:53:28 +0000 Subject: [PATCH 2/7] Support overrideWith(provider) and call providers always with call() when inserting them into the widget tree --- docs/src/content/docs/core/modals.mdx | 2 +- .../docs/core/provider-retrieval-process.mdx | 31 +- docs/src/content/docs/core/providers.mdx | 25 + docs/src/content/docs/core/scoped-di.mdx | 15 +- docs/src/content/docs/core/testing.mdx | 56 +- docs/src/content/docs/examples/auto-route.mdx | 2 +- docs/src/content/docs/examples/basic.mdx | 4 +- docs/src/content/docs/examples/bloc.mdx | 2 +- docs/src/content/docs/examples/solidart.mdx | 8 +- docs/src/content/docs/index.mdx | 2 +- docs/src/content/docs/installing.md | 8 +- .../comparison-with-alternatives.mdx | 2 +- .../content/docs/miscellaneous/reactivity.md | 2 +- examples/auto_route/lib/pages/books.dart | 2 +- examples/bloc/lib/main.dart | 2 +- examples/solidart/lib/pages/todos.dart | 2 +- examples/solidart/lib/widgets/todos_body.dart | 2 +- examples/solidart/test/widget_test.dart | 4 +- packages/disco/.gitignore | 45 ++ packages/disco/.metadata | 30 + packages/disco/CHANGELOG.md | 5 + packages/disco/README.md | 4 +- .../disco/benchmark/provider_benchmark.dart | 83 ++- packages/disco/benchmark_results.md | 21 + packages/disco/example/README.md | 29 +- packages/disco/example/lib/main.dart | 698 +++++++++++++++++- packages/disco/example/test/disco_test.dart | 2 +- packages/disco/lib/main.dart | 20 + packages/disco/lib/src/disco_internal.dart | 6 +- .../lib/src/models/overrides/override.dart | 32 + .../overrides/provider_argument_override.dart | 67 -- .../models/overrides/provider_override.dart | 57 -- ...ovider_argument.dart => arg_provider.dart} | 27 +- .../providers/instantiable_provider.dart | 31 +- .../{provider.dart => no_arg_provider.dart} | 24 +- .../disco/lib/src/widgets/provider_scope.dart | 254 +++++-- packages/disco/pubspec.yaml | 3 +- packages/disco/test/disco_test.dart | 291 ++++++-- 38 files changed, 1517 insertions(+), 383 deletions(-) create mode 100644 packages/disco/.gitignore create mode 100644 packages/disco/.metadata create mode 100644 packages/disco/benchmark_results.md create mode 100644 packages/disco/lib/main.dart delete mode 100644 packages/disco/lib/src/models/overrides/provider_argument_override.dart delete mode 100644 packages/disco/lib/src/models/overrides/provider_override.dart rename packages/disco/lib/src/models/providers/{provider_argument.dart => arg_provider.dart} (73%) rename packages/disco/lib/src/models/providers/{provider.dart => no_arg_provider.dart} (87%) diff --git a/docs/src/content/docs/core/modals.mdx b/docs/src/content/docs/core/modals.mdx index d5fdef9..44a7952 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 e8d820b..896f421 100644 --- a/docs/src/content/docs/core/provider-retrieval-process.mdx +++ b/docs/src/content/docs/core/provider-retrieval-process.mdx @@ -16,7 +16,7 @@ Refer to the following graph to understand how the providers are retrieved. 2. If it does, its internal map of overridden providers is checked to see if the provider is there. -3. If is there, the overridden value is returned. +3. If is there, the value of the overriding provider is returned. 4. Otherwise, the search continues for the first `ProviderScope` ancestor. @@ -35,3 +35,32 @@ Refer to the following graph to understand how the providers are retrieved. + +## 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 | 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` | + +## Providers with an argument and overrides + +An argument provider cannot be instantiated by the `ProviderScopeOverride` itself, since no argument is available there: the argument only exists where the provider is inserted into the widget tree. Therefore, a `ProviderScopeOverride` merely *registers* the overrides of argument providers, and every `ProviderScope` below it consults that registry while generating its intermediate providers. + +In other words, for providers with an argument, the retrieval process skips steps 1-3 above: there is nothing to look up in the `ProviderScopeOverride`, because the override has already been applied when the provider was inserted into the scope. + + diff --git a/docs/src/content/docs/core/providers.mdx b/docs/src/content/docs/core/providers.mdx index 3e5923c..98e459a 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. + + ### 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. @@ -87,3 +91,24 @@ There are also two optional named parameters that can be specified. | -------------- | ------- | ----------- | | `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, disposed and whether it is lazy. 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 c782b23..e8f6fc6 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,13 @@ ProviderScope( ) ``` + + ### 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 +72,7 @@ runApp( MaterialApp( home: Scaffold( body: ProviderScope( - providers: [numberProvider, doubleNumberPlusArgProvider], + providers: [numberProvider(), doubleNumberPlusArgProvider(10)], child: Builder( builder: (context) { final number = numberProvider.of(context); @@ -96,7 +103,7 @@ Therefore, the following code will lead to a `ProviderForwardReferenceError`: ProviderScope( providers: [ doubleNumberPlusArgProvider(10), - numberProvider, + numberProvider(), ], child: // ... ) @@ -107,7 +114,7 @@ The error can be prevented by reordering the list of providers: ```dart title="Correct example" ProviderScope( providers: [ - numberProvider, + numberProvider(), doubleNumberPlusArgProvider(10), ], child: // ... diff --git a/docs/src/content/docs/core/testing.mdx b/docs/src/content/docs/core/testing.mdx index 4bf0c88..d5ce9e9 100644 --- a/docs/src/content/docs/core/testing.mdx +++ b/docs/src/content/docs/core/testing.mdx @@ -5,7 +5,9 @@ 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`, `dispose` and `lazy` parameters. This means a mock can, for instance, inject other providers through its context, exactly like the provider it replaces.