From 5ba44abe07a6fa32e83da927f5094ee85cf3ead0 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Sun, 8 Dec 2024 18:21:48 +0100 Subject: [PATCH] Add mutations (#3860) fixes #1660 --- .../lib/src/core/consumer.dart | 45 +- packages/riverpod/CHANGELOG.md | 6 + packages/riverpod/lib/riverpod.dart | 4 +- .../riverpod/lib/src/common/listenable.dart | 6 +- packages/riverpod/lib/src/core/element.dart | 48 +- .../riverpod/lib/src/core/foundation.dart | 32 +- .../lib/src/core/modifiers/future.dart | 15 +- .../lib/src/core/modifiers/select.dart | 53 +- .../lib/src/core/modifiers/select_async.dart | 19 +- .../src/core/provider/notifier_provider.dart | 2 +- .../lib/src/core/provider/provider.dart | 8 +- .../lib/src/core/provider_container.dart | 131 +- .../lib/src/core/provider_subscription.dart | 290 ++- .../src/core/proxy_provider_listenable.dart | 115 +- packages/riverpod/lib/src/framework.dart | 1 + packages/riverpod/lib/src/mutation.dart | 513 ++++++ .../legacy/state_notifier_provider.dart | 4 +- .../src/providers/legacy/state_provider.dart | 4 +- packages/riverpod/test/old/utils.dart | 106 +- .../test/src/core/provider_element_test.dart | 4 +- .../test/src/core/provider_observer_test.dart | 329 ++-- packages/riverpod/test/src/core/ref_test.dart | 42 +- packages/riverpod/test/src/utils.dart | 67 +- packages/riverpod_analyzer_utils/CHANGELOG.md | 1 + .../lib/src/errors.dart | 3 + .../lib/src/nodes.dart | 3 +- .../nodes/{riverpod.dart => annotation.dart} | 69 + .../lib/src/nodes/providers/notifier.dart | 125 +- .../lib/src/riverpod_types/generator.dart | 6 + .../test/mutation_test.dart | 98 + packages/riverpod_annotation/CHANGELOG.md | 3 + .../riverpod_annotation/analysis_options.yaml | 6 + .../lib/riverpod_annotation.dart | 7 + packages/riverpod_generator/CHANGELOG.md | 2 +- .../lib/src/riverpod_generator.dart | 32 +- .../lib/src/templates/element.dart | 95 + .../lib/src/templates/mutation.dart | 100 ++ .../lib/src/templates/provider.dart | 49 +- packages/riverpod_generator/pubspec.yaml | 1 + .../test/analysis_options.yaml | 5 + .../test/integration/mutation.dart | 149 ++ .../test/integration/mutation.g.dart | 1593 +++++++++++++++++ packages/riverpod_generator/test/mock.dart | 201 +++ .../test/mutation_test.dart | 513 ++++++ .../concepts/provider_observer_logger.dart | 5 +- .../provider_observer/provider_observer.dart | 24 +- 46 files changed, 4405 insertions(+), 529 deletions(-) create mode 100644 packages/riverpod/lib/src/mutation.dart rename packages/riverpod_analyzer_utils/lib/src/nodes/{riverpod.dart => annotation.dart} (68%) create mode 100644 packages/riverpod_analyzer_utils_tests/test/mutation_test.dart create mode 100644 packages/riverpod_annotation/analysis_options.yaml create mode 100644 packages/riverpod_generator/lib/src/templates/element.dart create mode 100644 packages/riverpod_generator/lib/src/templates/mutation.dart create mode 100644 packages/riverpod_generator/test/analysis_options.yaml create mode 100644 packages/riverpod_generator/test/integration/mutation.dart create mode 100644 packages/riverpod_generator/test/integration/mutation.g.dart create mode 100644 packages/riverpod_generator/test/mock.dart create mode 100644 packages/riverpod_generator/test/mutation_test.dart diff --git a/packages/flutter_riverpod/lib/src/core/consumer.dart b/packages/flutter_riverpod/lib/src/core/consumer.dart index 853bc2ea7..e8edf5f5d 100644 --- a/packages/flutter_riverpod/lib/src/core/consumer.dart +++ b/packages/flutter_riverpod/lib/src/core/consumer.dart @@ -367,7 +367,7 @@ class ConsumerStatefulElement extends StatefulElement implements WidgetRef { Map, ProviderSubscription>? _oldDependencies; final _listeners = >[]; - List<_ListenManual>? _manualListeners; + List>? _manualListeners; bool? _visible; Iterable get _allSubscriptions sync* { @@ -532,14 +532,20 @@ class ConsumerStatefulElement extends StatefulElement implements WidgetRef { // be used inside initState. final container = ProviderScope.containerOf(this, listen: false); - final sub = _ListenManual( - container.listen( - provider, - listener, - onError: onError, - fireImmediately: fireImmediately, - ) as ProviderSubscriptionWithOrigin, - this, + final innerSubscription = container.listen( + provider, + listener, + onError: onError, + fireImmediately: fireImmediately, + // ignore: invalid_use_of_internal_member, from riverpod + ) as ProviderSubscriptionWithOrigin; + + // ignore: invalid_use_of_internal_member, from riverpod + late final ProviderSubscriptionView sub; + sub = ProviderSubscriptionView( + innerSubscription: innerSubscription, + onClose: () => _manualListeners?.remove(sub), + read: innerSubscription.read, ); _applyVisibility(sub); listeners.add(sub); @@ -547,24 +553,3 @@ class ConsumerStatefulElement extends StatefulElement implements WidgetRef { return sub; } } - -final class _ListenManual - // ignore: invalid_use_of_internal_member - extends DelegatingProviderSubscription { - _ListenManual(this.innerSubscription, this._element); - - @override - final ProviderSubscriptionWithOrigin innerSubscription; - final ConsumerStatefulElement _element; - - @override - void close() { - if (!closed) { - _element._manualListeners?.remove(this); - } - super.close(); - } - - @override - T read() => innerSubscription.read(); -} diff --git a/packages/riverpod/CHANGELOG.md b/packages/riverpod/CHANGELOG.md index 7c42ef9e4..d5fe7418c 100644 --- a/packages/riverpod/CHANGELOG.md +++ b/packages/riverpod/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased build +- **Breaking**: ProviderObserver methods have been updated to take a `ProviderObserverContext` parameter. + This replaces the old `provider`+`container` parameters, and contains extra + information. - **Breaking**: It is now a runtime exception to "scope" a provider that is not specifying `dependencies`. - **Breaking**: Removed all `Ref` subclasses (such `FutureProviderRef`). @@ -21,6 +24,9 @@ - **Breaking**: A provider is now considered "paused" if all of its listeners are also paused. So if a provider `A` is watched _only_ by a provider `B`, and `B` is currently unused, then `A` will be paused. +- Added methods to `ProviderObserver` for listening to "mutations". + Mutations are a new code-generation-only feature. See riverpod_generator's changelog + for more information. - Added `Ref.listen(..., weak: true)`. When specifying `weak: true`, the listener will not cause the provider to be initialized. This is useful when wanting to react to changes to a provider, diff --git a/packages/riverpod/lib/riverpod.dart b/packages/riverpod/lib/riverpod.dart index f32aac134..439a701d0 100644 --- a/packages/riverpod/lib/riverpod.dart +++ b/packages/riverpod/lib/riverpod.dart @@ -36,12 +36,12 @@ export 'src/framework.dart' AsyncSubscription, FutureModifierElement, RunNotifierBuild, + ProviderListenableWithOrigin, $FunctionalProvider, ProviderStateSubscription, ProviderSubscriptionImpl, ProviderSubscriptionWithOrigin, - SelectorSubscription, - DelegatingProviderSubscription, + ProviderSubscriptionView, $ClassProvider, LegacyProviderMixin, ClassProviderElement, diff --git a/packages/riverpod/lib/src/common/listenable.dart b/packages/riverpod/lib/src/common/listenable.dart index 7284f7c37..7a615a3ba 100644 --- a/packages/riverpod/lib/src/common/listenable.dart +++ b/packages/riverpod/lib/src/common/listenable.dart @@ -51,6 +51,8 @@ class ProxyElementValueListenable extends _ValueListenable { } class _ValueListenable { + void Function()? onCancel; + int _count = 0; // The _listeners is intentionally set to a fixed-length _GrowableList instead // of const []. @@ -93,7 +95,6 @@ class _ValueListenable { /// [_notifyListeners]; and similarly, by overriding [_removeListener], checking /// if [hasListeners] is false after calling `super.removeListener()`, and if /// so, stopping that same work. - @protected bool get hasListeners { return _count > 0; } @@ -218,6 +219,9 @@ class _ValueListenable { break; } } + + final onCancel = this.onCancel; + if (!hasListeners && onCancel != null) onCancel(); } /// Discards any resources used by the object. After this is called, the diff --git a/packages/riverpod/lib/src/core/element.dart b/packages/riverpod/lib/src/core/element.dart index e8b408d91..58dd8045d 100644 --- a/packages/riverpod/lib/src/core/element.dart +++ b/packages/riverpod/lib/src/core/element.dart @@ -18,8 +18,9 @@ part of '../framework.dart'; /// {@endtemplate} sealed class Refreshable implements ProviderListenable {} -mixin _ProviderRefreshable implements Refreshable { - ProviderBase get provider; +mixin _ProviderRefreshable + implements Refreshable, ProviderListenableWithOrigin { + ProviderBase get provider; } /// A debug utility used by `flutter_riverpod`/`hooks_riverpod` to check @@ -137,7 +138,6 @@ abstract class ProviderElement implements Node { /// /// This is not meant for public consumption. Instead, public API should use /// [readSelf]. - @internal Result? get stateResult => _stateResult; /// Returns the currently exposed by a provider @@ -156,7 +156,6 @@ abstract class ProviderElement implements Node { /// /// This API is not meant for public consumption. Instead if a [Ref] needs /// to expose a way to update the state, the practice is to expose a getter/setter. - @internal void setStateResult(Result newState) { if (kDebugMode) _debugDidSetState = true; @@ -175,7 +174,6 @@ abstract class ProviderElement implements Node { /// /// This is not meant for public consumption. Instead, public API should use /// [readSelf]. - @internal StateT get requireState { const uninitializedError = ''' Tried to read the state of an uninitialized provider. @@ -204,7 +202,6 @@ This could mean a few things: /// Called when a provider is rebuilt. Used for providers to not notify their /// listeners if the exposed value did not change. - @internal bool updateShouldNotify(StateT previous, StateT next); /* /STATE */ @@ -225,7 +222,7 @@ This could mean a few things: } /// Called the first time a provider is obtained. - void _mount() { + void mount() { if (kDebugMode) { _debugCurrentCreateHash = provider.debugGetCreateSourceHash(); } @@ -298,11 +295,10 @@ This could mean a few things: /// /// This is not meant for public consumption. Public API should hide /// [flush] from users, such that they don't need to care about invoking this function. - @internal void flush() { if (!_didMount) { _didMount = true; - _mount(); + mount(); } _maybeRebuildDependencies(); @@ -420,6 +416,17 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu ); } + MutationContext? _currentMutationContext() => + Zone.current[mutationZoneKey] as MutationContext?; + + ProviderObserverContext _currentObserverContext() { + return ProviderObserverContext( + origin, + container, + mutation: _currentMutationContext(), + ); + } + void _notifyListeners( Result newState, Result? previousStateResult, { @@ -475,7 +482,7 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu if (listener.closed) continue; Zone.current.runBinaryGuarded( - listener._notify, + listener._onOriginData, previousState, newState.state, ); @@ -486,7 +493,7 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu if (listener.closed) continue; Zone.current.runBinaryGuarded( - listener._notifyError, + listener._onOriginError, newState.error, newState.stackTrace, ); @@ -495,31 +502,28 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu for (final observer in container.observers) { if (isMount) { - runTernaryGuarded( + runBinaryGuarded( observer.didAddProvider, - origin, + _currentObserverContext(), newState.stateOrNull, - container, ); } else { - runQuaternaryGuarded( + runTernaryGuarded( observer.didUpdateProvider, - origin, + _currentObserverContext(), previousState, newState.stateOrNull, - container, ); } } for (final observer in container.observers) { if (newState is ResultError) { - runQuaternaryGuarded( + runTernaryGuarded( observer.providerDidFail, - origin, + _currentObserverContext(), newState.error, newState.stackTrace, - container, ); } } @@ -564,7 +568,7 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu ); switch (sub) { - case ProviderSubscriptionImpl(): + case final ProviderSubscriptionImpl sub: sub._listenedElement.addDependentSubscription(sub); } @@ -715,7 +719,7 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu ref._onDisposeListeners?.forEach(runGuarded); for (final observer in container.observers) { - runBinaryGuarded(observer.didDisposeProvider, origin, container); + runUnaryGuarded(observer.didDisposeProvider, _currentObserverContext()); } ref._keepAliveLinks = null; diff --git a/packages/riverpod/lib/src/core/foundation.dart b/packages/riverpod/lib/src/core/foundation.dart index fde25aab8..89c7abd9c 100644 --- a/packages/riverpod/lib/src/core/foundation.dart +++ b/packages/riverpod/lib/src/core/foundation.dart @@ -176,6 +176,29 @@ String shortHash(Object? object) { return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); } +@internal +mixin ProviderListenableWithOrigin on ProviderListenable { + @override + ProviderSubscriptionWithOrigin addListener( + Node source, + void Function(OutT? previous, OutT next) listener, { + required void Function(Object error, StackTrace stackTrace)? onError, + required void Function()? onDependencyMayHaveChanged, + required bool fireImmediately, + required bool weak, + }); + + @override + ProviderListenable select( + Selected Function(OutT value) selector, + ) { + return _ProviderSelector( + provider: this, + selector: selector, + ); + } +} + /// A base class for all providers, used to consume a provider. /// /// It is used by [ProviderContainer.listen] and `ref.watch` to listen to @@ -185,7 +208,7 @@ String shortHash(Object? object) { @immutable mixin ProviderListenable implements ProviderListenableOrFamily { /// Starts listening to this transformer - ProviderSubscriptionWithOrigin addListener( + ProviderSubscription addListener( Node source, void Function(StateT? previous, StateT next) listener, { required void Function(Object error, StackTrace stackTrace)? onError, @@ -261,10 +284,5 @@ mixin ProviderListenable implements ProviderListenableOrFamily { /// changed instead of whenever the age changes. ProviderListenable select( Selected Function(StateT value) selector, - ) { - return _ProviderSelector( - provider: this, - selector: selector, - ); - } + ); } diff --git a/packages/riverpod/lib/src/core/modifiers/future.dart b/packages/riverpod/lib/src/core/modifiers/future.dart index 5a0d4bbb8..757518867 100644 --- a/packages/riverpod/lib/src/core/modifiers/future.dart +++ b/packages/riverpod/lib/src/core/modifiers/future.dart @@ -138,8 +138,10 @@ base mixin $FutureModifier on ProviderBase> { /// return await http.get('${configs.host}/products'); /// }); /// ``` - Refreshable> get future { - return ProviderElementProxy, Future>( + Refreshable> get future => _future; + + _ProviderRefreshable, AsyncValue> get _future { + return ProviderElementProxy, AsyncValue>( this, (element) { element as FutureModifierElement; @@ -181,10 +183,10 @@ base mixin $FutureModifier on ProviderBase> { ProviderListenable> selectAsync( Output Function(StateT data) selector, ) { - return _AsyncSelector( + return _AsyncSelector>( selector: selector, provider: this, - future: future, + future: _future, ); } } @@ -290,12 +292,11 @@ mixin FutureModifierElement on ProviderElement> { asyncTransition(value, seamless: seamless); for (final observer in container.observers) { - runQuaternaryGuarded( + runTernaryGuarded( observer.providerDidFail, - provider, + _currentObserverContext(), value.error, value.stackTrace, - container, ); } diff --git a/packages/riverpod/lib/src/core/modifiers/select.dart b/packages/riverpod/lib/src/core/modifiers/select.dart index 474eb13a8..b9bf77388 100644 --- a/packages/riverpod/lib/src/core/modifiers/select.dart +++ b/packages/riverpod/lib/src/core/modifiers/select.dart @@ -23,7 +23,10 @@ var _debugIsRunningSelector = false; /// An internal class for `ProviderBase.select`. @sealed -class _ProviderSelector with ProviderListenable { +class _ProviderSelector + with + ProviderListenable, + ProviderListenableWithOrigin { /// An internal class for `ProviderBase.select`. _ProviderSelector({ required this.provider, @@ -31,7 +34,7 @@ class _ProviderSelector with ProviderListenable { }); /// The provider that was selected - final ProviderListenable provider; + final ProviderListenableWithOrigin provider; /// The selector applied final OutputT Function(InputT) selector; @@ -78,7 +81,7 @@ class _ProviderSelector with ProviderListenable { } @override - SelectorSubscription addListener( + ProviderSubscriptionWithOrigin addListener( Node node, void Function(OutputT? previous, OutputT next) listener, { required void Function(Object error, StackTrace stackTrace)? onError, @@ -116,9 +119,12 @@ class _ProviderSelector with ProviderListenable { ); } - return SelectorSubscription( + return ProviderSubscriptionView( innerSubscription: sub, - () { + read: () { + // flushes the provider + sub.read(); + // Using ! because since `sub.read` flushes the inner subscription, // it is guaranteed that `lastSelectedValue` is not null. return switch (lastSelectedValue!) { @@ -133,40 +139,3 @@ class _ProviderSelector with ProviderListenable { ); } } - -@internal -final class SelectorSubscription - extends DelegatingProviderSubscription { - SelectorSubscription( - this._read, { - this.onClose, - required this.innerSubscription, - }); - - @override - final ProviderSubscriptionWithOrigin innerSubscription; - - final void Function()? onClose; - final OutT Function() _read; - - @override - void close() { - if (closed) return; - - onClose?.call(); - super.close(); - } - - @override - OutT read() { - if (closed) { - throw StateError( - 'called ProviderSubscription.read on a subscription that was closed', - ); - } - // flushes the provider - innerSubscription.read(); - - return _read(); - } -} diff --git a/packages/riverpod/lib/src/core/modifiers/select_async.dart b/packages/riverpod/lib/src/core/modifiers/select_async.dart index 30cff870e..ffb3bd42c 100644 --- a/packages/riverpod/lib/src/core/modifiers/select_async.dart +++ b/packages/riverpod/lib/src/core/modifiers/select_async.dart @@ -2,7 +2,10 @@ part of '../../framework.dart'; /// An internal class for `ProviderBase.selectAsync`. @sealed -class _AsyncSelector with ProviderListenable> { +class _AsyncSelector + with + ProviderListenable>, + ProviderListenableWithOrigin, OriginT> { /// An internal class for `ProviderBase.select`. _AsyncSelector({ required this.provider, @@ -11,10 +14,10 @@ class _AsyncSelector with ProviderListenable> { }); /// The provider that was selected - final ProviderListenable> provider; + final ProviderListenableWithOrigin, OriginT> provider; /// The future associated to the listened provider - final ProviderListenable> future; + final ProviderListenableWithOrigin, OriginT> future; /// The selector applied final OutputT Function(InputT) selector; @@ -32,7 +35,7 @@ class _AsyncSelector with ProviderListenable> { } @override - SelectorSubscription, Future> addListener( + ProviderSubscriptionWithOrigin, OriginT> addListener( Node node, void Function(Future? previous, Future next) listener, { required void Function(Object error, StackTrace stackTrace)? onError, @@ -147,9 +150,13 @@ class _AsyncSelector with ProviderListenable> { listener(null, selectedFuture!); } - return SelectorSubscription( + return ProviderSubscriptionView, OriginT>( innerSubscription: sub, - () => selectedFuture!, + read: () { + // Flush + sub.read(); + return selectedFuture!; + }, onClose: () { final completer = selectedCompleter; if (completer != null && !completer.isCompleted) { diff --git a/packages/riverpod/lib/src/core/provider/notifier_provider.dart b/packages/riverpod/lib/src/core/provider/notifier_provider.dart index db3ef4566..c671fcdea 100644 --- a/packages/riverpod/lib/src/core/provider/notifier_provider.dart +++ b/packages/riverpod/lib/src/core/provider/notifier_provider.dart @@ -121,7 +121,7 @@ abstract base class $ClassProvider< // }); Refreshable get notifier { - return ProviderElementProxy( + return ProviderElementProxy( this, (element) => (element as ClassProviderElement) diff --git a/packages/riverpod/lib/src/core/provider/provider.dart b/packages/riverpod/lib/src/core/provider/provider.dart index 866e5bb19..2540fd376 100644 --- a/packages/riverpod/lib/src/core/provider/provider.dart +++ b/packages/riverpod/lib/src/core/provider/provider.dart @@ -17,13 +17,15 @@ typedef Create = CreatedT Function(Ref ref); /// A callback used to catches errors @internal -typedef OnError = void Function(Object, StackTrace); +typedef OnError = void Function(Object error, StackTrace stackTrace); /// A base class for _all_ providers. @immutable // Marked as "base" because linters/generators rely on fields on const provider instances. abstract base class ProviderBase extends ProviderOrFamily - with ProviderListenable + with + ProviderListenable, + ProviderListenableWithOrigin implements Refreshable, _ProviderOverride { /// A base class for _all_ providers. const ProviderBase({ @@ -81,7 +83,7 @@ abstract base class ProviderBase extends ProviderOrFamily source: source, listenedElement: element, weak: weak, - listener: (prev, next) => listener(prev as StateT?, next as StateT), + listener: listener, onError: onError, ); } diff --git a/packages/riverpod/lib/src/core/provider_container.dart b/packages/riverpod/lib/src/core/provider_container.dart index 05be79843..4954a782e 100644 --- a/packages/riverpod/lib/src/core/provider_container.dart +++ b/packages/riverpod/lib/src/core/provider_container.dart @@ -778,7 +778,7 @@ class ProviderContainer implements Node { ); switch (sub) { - case ProviderSubscriptionImpl(): + case final ProviderSubscriptionImpl sub: sub._listenedElement.addDependentSubscription(sub); } @@ -804,16 +804,12 @@ class ProviderContainer implements Node { /// {@macro riverpod.refresh} StateT refresh(Refreshable refreshable) { - ProviderBase providerToRefresh; - - switch (refreshable) { - case ProviderBase(): - providerToRefresh = refreshable; - case _ProviderRefreshable(:final provider): - providerToRefresh = provider; - } - + final providerToRefresh = switch (refreshable) { + ProviderBase() => refreshable, + _ProviderRefreshable(:final provider) => provider + }; invalidate(providerToRefresh); + return read(refreshable); } @@ -977,6 +973,7 @@ class ProviderContainer implements Node { void dispose() => _dispose(updateChildren: true); /// Traverse the [ProviderElement]s associated with this [ProviderContainer]. + @internal Iterable getAllProviderElements() { return _pointerManager .listProviderPointers() @@ -990,6 +987,7 @@ class ProviderContainer implements Node { /// This is fairly expensive and should be avoided as much as possible. /// If you do not need for providers to be sorted, consider using [getAllProviderElements] /// instead, which returns an unsorted list and is significantly faster. + @internal Iterable getAllProviderElementsInOrder() sync* { final visitedNodes = HashSet(); final queue = DoubleLinkedQueue(); @@ -1064,6 +1062,44 @@ extension ProviderContainerTest on ProviderContainer { ProviderPointerManager get pointerManager => _pointerManager; } +/// Information about the pending mutation, when [ProviderObserver] emits +/// an event while a mutation is in progress. +class MutationContext { + @internal + MutationContext(this.invocation, this.notifier); + + /// Information about the method invoked by the mutation, and its arguments. + final Invocation invocation; + + /// The notifier that triggered the mutation. + final NotifierBase notifier; +} + +/// Information about the [ProviderObserver] event. +class ProviderObserverContext { + @internal + ProviderObserverContext( + this.provider, + this.container, { + this.mutation, + }); + + /// The provider that triggered the event. + final ProviderBase provider; + + /// The container that owns [provider]'s state. + final ProviderContainer container; + + /// The pending mutation while the observer was called. + /// + /// Pretty much all observer events may be triggered by a mutation under some + /// conditions. + /// For example, if a mutation refreshes another provider, then + /// [ProviderObserver.didDisposeProvider] will contain the mutation that + /// disposed the provider. + final MutationContext? mutation; +} + /// An object that listens to the changes of a [ProviderContainer]. /// /// This can be used for logging or making devtools. @@ -1077,35 +1113,92 @@ abstract class ProviderObserver { /// /// [value] will be `null` if the provider threw during initialization. void didAddProvider( - ProviderBase provider, + ProviderObserverContext context, Object? value, - ProviderContainer container, ) {} /// A provider emitted an error, be it by throwing during initialization /// or by having a [Future]/[Stream] emit an error void providerDidFail( - ProviderBase provider, + ProviderObserverContext context, Object error, StackTrace stackTrace, - ProviderContainer container, ) {} /// Called by providers when they emit a notification. /// /// - [newValue] will be `null` if the provider threw during initialization. /// - [previousValue] will be `null` if the previous build threw during initialization. + /// + /// If the change is caused by a "mutation", [mutation] will be the invocation + /// that caused the state change. + /// This includes when a mutation manually calls `state=`: + /// + /// ```dart + /// @riverpod + /// class Example extends _$Example { + /// @override + /// int count() => 0; + /// + /// @mutation + /// int increment() { + /// state++; // This will trigger `didUpdateProvider` and "mutation" will be `#increment` + /// + /// // ... + /// } + /// } + /// ``` void didUpdateProvider( - ProviderBase provider, + ProviderObserverContext context, Object? previousValue, Object? newValue, - ProviderContainer container, ) {} /// A provider was disposed - void didDisposeProvider( - ProviderBase provider, - ProviderContainer container, + void didDisposeProvider(ProviderObserverContext context) {} + + /// A mutation was reset. + /// + /// This includes both manual calls to [MutationBase.reset] and automatic + /// resets. + /// + /// {@macro auto_reset} + void mutationReset(ProviderObserverContext context) {} + + /// A mutation was started. + /// + /// {@template obs_mutation_arg} + /// [mutation] is strictly the same as [ProviderObserverContext.mutation]. + /// It is provided as a convenience, as this life-cycle is guaranteed + /// to have a non-null [ProviderObserverContext.mutation]. + /// {@endtemplate} + void mutationStart( + ProviderObserverContext context, + MutationContext mutation, + ) {} + + /// A mutation failed. + /// + /// [error] is the error thrown by the mutation. + /// [stackTrace] is the stack trace of the error. + /// + /// {@macro obs_mutation_arg} + void mutationError( + ProviderObserverContext context, + MutationContext mutation, + Object error, + StackTrace stackTrace, + ) {} + + /// A mutation succeeded. + /// + /// [result] is the value returned by the mutation. + /// + /// {@macro obs_mutation_arg} + void mutationSuccess( + ProviderObserverContext context, + MutationContext mutation, + Object? result, ) {} } diff --git a/packages/riverpod/lib/src/core/provider_subscription.dart b/packages/riverpod/lib/src/core/provider_subscription.dart index c1440738c..421a72790 100644 --- a/packages/riverpod/lib/src/core/provider_subscription.dart +++ b/packages/riverpod/lib/src/core/provider_subscription.dart @@ -31,21 +31,36 @@ sealed class ProviderSubscription { void close(); } +@internal @optionalTypeArgs sealed class ProviderSubscriptionWithOrigin - extends ProviderSubscription { + extends ProviderSubscription implements Pausable { ProviderBase get origin; ProviderElement get _listenedElement; - void _notify(StateT? prev, StateT next); + void _onOriginData(StateT? prev, StateT next); + void _onOriginError(Object error, StackTrace stackTrace); + + OutT _callRead(); + + @override + OutT read() { + if (closed) { + throw StateError( + 'called ProviderSubscription.read on a subscription that was closed', + ); + } + _listenedElement.mayNeedDispose(); + _listenedElement.flush(); - void _notifyError(Object error, StackTrace stackTrace); + return _callRead(); + } } @internal @optionalTypeArgs -abstract base class ProviderSubscriptionImpl - extends ProviderSubscriptionWithOrigin with _OnPauseMixin { +abstract base class ProviderSubscriptionImpl + extends ProviderSubscriptionWithOrigin with _OnPauseMixin { @override bool get isPaused => _isPaused; @@ -54,6 +69,59 @@ abstract base class ProviderSubscriptionImpl bool get closed => _closed; var _closed = false; + /// Whether an event was sent while this subscription was paused. + /// + /// This enables re-rending the last missing event when the subscription is resumed. + ({ + (OutT?, OutT)? data, + (Object, StackTrace)? error, + })? _missedCalled; + void Function(OutT? prev, OutT next) get _listener; + OnError get _errorListener; + + @mustCallSuper + @override + void onCancel() { + _listenedElement.onSubscriptionPause(this); + } + + @mustCallSuper + @override + void onResume() { + _listenedElement.onSubscriptionResume(this); + if (_missedCalled?.data case final event?) { + final prev = event.$1; + final next = event.$2; + + _missedCalled = null; + _notifyData(prev, next); + } else if (_missedCalled?.error case final event?) { + final error = event.$1; + final stackTrace = event.$2; + + _missedCalled = null; + _notifyError(error, stackTrace); + } + } + + void _notifyData(OutT? prev, OutT next) { + if (isPaused) { + _missedCalled = (data: (prev, next), error: null); + return; + } + + _listener(prev, next); + } + + void _notifyError(Object error, StackTrace stackTrace) { + if (isPaused) { + _missedCalled = (data: null, error: (error, stackTrace)); + return; + } + + _errorListener(error, stackTrace); + } + /// Stops listening to the provider. /// /// It is safe to call this method multiple times. @@ -67,42 +135,80 @@ abstract base class ProviderSubscriptionImpl } } -mixin _OnPauseMixin { +abstract class Pausable { + bool get isPaused; + + void pause(); + void resume(); + + void onCancel(); + void onResume(); +} + +mixin _OnPauseMixin on Pausable { bool get _isPaused => _pauseCount > 0; var _pauseCount = 0; + @override @mustCallSuper void pause() { - if (_pauseCount == 0) { - onCancel(); - } + final shouldCallCancel = _pauseCount == 0; _pauseCount++; + + if (shouldCallCancel) onCancel(); } + @override @mustCallSuper void resume() { - if (_pauseCount == 1) { - onResume(); - } + final shouldCallResume = _pauseCount == 1; _pauseCount = math.max(_pauseCount - 1, 0); + + if (shouldCallResume) onResume(); } + @override void onResume(); + @override void onCancel(); } @internal -abstract base class DelegatingProviderSubscription - extends ProviderSubscriptionImpl { +base class ProviderSubscriptionView + extends ProviderSubscriptionImpl { + ProviderSubscriptionView({ + required this.innerSubscription, + required OutT Function() read, + void Function()? onClose, + }) : _read = read, + _onClose = onClose; + + final ProviderSubscriptionWithOrigin innerSubscription; + final OutT Function() _read; + final void Function()? _onClose; + @override - ProviderBase get origin => innerSubscription.origin; + OnError get _errorListener { + return switch (innerSubscription) { + final ProviderSubscriptionImpl sub => + sub._errorListener, + }; + } @override - ProviderElement get _listenedElement => - innerSubscription._listenedElement; + void Function(OutT? prev, OutT next) get _listener { + return switch (innerSubscription) { + final ProviderSubscriptionImpl sub => sub._listener, + }; + } + + @override + ProviderBase get origin => innerSubscription.origin; - ProviderSubscriptionWithOrigin get innerSubscription; + @override + ProviderElement get _listenedElement => + innerSubscription._listenedElement; @override bool get weak => innerSubscription.weak; @@ -111,27 +217,29 @@ abstract base class DelegatingProviderSubscription Node get source => innerSubscription.source; @override - void _notify(StateT? prev, StateT next) { - innerSubscription._notify(prev, next); + void _onOriginData(OriginT? prev, OriginT next) { + innerSubscription._onOriginData(prev, next); } @override - void _notifyError(Object error, StackTrace stackTrace) { - innerSubscription._notifyError(error, stackTrace); + void _onOriginError(Object error, StackTrace stackTrace) { + innerSubscription._onOriginError(error, stackTrace); } @override void onCancel() { + super.onCancel(); switch (innerSubscription) { - case final ProviderSubscriptionImpl sub: + case final ProviderSubscriptionImpl sub: sub.onCancel(); } } @override void onResume() { + super.onResume(); switch (innerSubscription) { - case final ProviderSubscriptionImpl sub: + case final ProviderSubscriptionImpl sub: sub.onResume(); } } @@ -152,22 +260,86 @@ abstract base class DelegatingProviderSubscription void close() { if (_closed) return; + _onClose?.call(); innerSubscription.close(); super.close(); } + + @override + OutT _callRead() => _read(); +} + +final class DelegatingProviderSubscription + extends ProviderSubscriptionImpl { + DelegatingProviderSubscription({ + required this.origin, + required this.source, + required this.weak, + required OnError? errorListener, + required ProviderElement listenedElement, + required void Function(OutT? prev, OutT next) listener, + void Function(OriginT? prev, OriginT next)? onOriginData, + void Function(Object error, StackTrace stackTrace)? onOriginError, + required OutT Function() read, + required void Function()? onClose, + }) : _errorListener = errorListener ?? Zone.current.handleUncaughtError, + _listenedElement = listenedElement, + _listener = listener, + _onOriginDataCb = onOriginData, + _onOriginErrorCb = onOriginError, + _readCb = read, + _onCloseCb = onClose; + + @override + final ProviderBase origin; + @override + final Node source; + @override + final bool weak; + @override + final OnError _errorListener; + @override + final ProviderElement _listenedElement; + @override + final void Function(OutT? prev, OutT next) _listener; + final void Function(OriginT? prev, OriginT next)? _onOriginDataCb; + final void Function(Object error, StackTrace stackTrace)? _onOriginErrorCb; + final OutT Function() _readCb; + final void Function()? _onCloseCb; + + @override + void _onOriginData(OriginT? prev, OriginT next) => + _onOriginDataCb?.call(prev, next); + + @override + void _onOriginError(Object error, StackTrace stackTrace) => + _onOriginErrorCb?.call(error, stackTrace); + + @override + OutT _callRead() => _readCb(); + + @override + void close() { + if (_closed) return; + + _onCloseCb?.call(); + super.close(); + } } /// When a provider listens to another provider using `listen` @internal final class ProviderStateSubscription - extends ProviderSubscriptionImpl with _OnPauseMixin { + extends ProviderSubscriptionImpl { ProviderStateSubscription({ required this.source, required this.weak, required ProviderElement listenedElement, - required this.listener, - required this.onError, - }) : _listenedElement = listenedElement; + required void Function(StateT? prev, StateT next) listener, + required OnError onError, + }) : _listenedElement = listenedElement, + _listener = listener, + _errorListener = onError; @override ProviderBase get origin => _listenedElement.origin; @@ -180,68 +352,18 @@ final class ProviderStateSubscription final bool weak; // Why can't this be typed correctly? - final void Function(Object? prev, Object? state) listener; - final OnError onError; - - /// Whether an event was sent while this subscription was paused. - /// - /// This enables re-rending the last missing event when the subscription is resumed. - ({ - (StateT?, StateT)? data, - (Object, StackTrace)? error, - })? _missedCalled; + final void Function(StateT? prev, StateT next) _listener; + final OnError _errorListener; @override - StateT read() { - if (_closed) { - throw StateError( - 'called ProviderSubscription.read on a subscription that was closed', - ); - } - _listenedElement.mayNeedDispose(); - return _listenedElement.readSelf(); - } + StateT _callRead() => _listenedElement.readSelf(); @override - void onCancel() => _listenedElement.onSubscriptionPause(this); + void _onOriginData(StateT? prev, StateT next) => _notifyData(prev, next); @override - void onResume() { - _listenedElement.onSubscriptionResume(this); - if (_missedCalled?.data case final event?) { - final prev = event.$1; - final next = event.$2; - - _missedCalled = null; - listener(prev, next); - } else if (_missedCalled?.error case final event?) { - final error = event.$1; - final stackTrace = event.$2; - - _missedCalled = null; - onError(error, stackTrace); - } - } - - @override - void _notify(StateT? prev, StateT next) { - if (_isPaused) { - _missedCalled = (data: (prev, next), error: null); - return; - } - - listener(prev, next); - } - - @override - void _notifyError(Object error, StackTrace stackTrace) { - if (_isPaused) { - _missedCalled = (data: null, error: (error, stackTrace)); - return; - } - - onError(error, stackTrace); - } + void _onOriginError(Object error, StackTrace stackTrace) => + _notifyError(error, stackTrace); } /// Deals with the internals of synchronously calling the listeners diff --git a/packages/riverpod/lib/src/core/proxy_provider_listenable.dart b/packages/riverpod/lib/src/core/proxy_provider_listenable.dart index ba7f741f9..94e9651b8 100644 --- a/packages/riverpod/lib/src/core/proxy_provider_listenable.dart +++ b/packages/riverpod/lib/src/core/proxy_provider_listenable.dart @@ -1,35 +1,56 @@ part of '../framework.dart'; -final class _ProxySubscription - extends DelegatingProviderSubscription { - _ProxySubscription( - this.innerSubscription, - this._removeListeners, - this._read, - ); +class LazyProxyListenable + with ProviderListenable, ProviderListenableWithOrigin { + LazyProxyListenable(this.provider, this._lense); - @override - final ProviderSubscriptionWithOrigin innerSubscription; - - final void Function() _removeListeners; - final OutT Function() _read; + final ProviderBase provider; + final ProxyElementValueListenable Function( + ProviderElement element, + ) _lense; @override - OutT read() { - if (closed) { - throw StateError( - 'called ProviderSubscription.read on a subscription that was closed', - ); + ProviderSubscriptionWithOrigin addListener( + Node source, + void Function(OutT? previous, OutT next) listener, { + required void Function(Object error, StackTrace stackTrace)? onError, + required void Function()? onDependencyMayHaveChanged, + required bool fireImmediately, + required bool weak, + }) { + final element = source.readProviderElement(provider); + + final listenable = _lense(element); + if (fireImmediately) { + switch (listenable.result) { + case null: + break; + case final ResultData data: + runBinaryGuarded(listener, null, data.state); + case final ResultError error: + if (onError != null) { + runBinaryGuarded(onError, error.error, error.stackTrace); + } + } } - return _read(); - } - @override - void close() { - if (closed) return; + late final ProviderSubscriptionImpl sub; + final removeListener = listenable.addListener( + (a, b) => sub._notifyData(a, b), + onError: onError, + onDependencyMayHaveChanged: onDependencyMayHaveChanged, + ); - _removeListeners(); - super.close(); + return sub = DelegatingProviderSubscription( + listenedElement: element, + source: source, + weak: weak, + origin: provider, + onClose: removeListener, + errorListener: onError, + listener: listener, + read: () => listenable.value, + ); } } @@ -49,8 +70,11 @@ final class _ProxySubscription /// /// This API is not meant for public consumption. @internal -class ProviderElementProxy - with ProviderListenable, _ProviderRefreshable { +class ProviderElementProxy + with + ProviderListenable, + ProviderListenableWithOrigin, + _ProviderRefreshable { /// An internal utility for reading alternate values of a provider. /// /// For example, this is used by [FutureProvider] to differentiate: @@ -66,16 +90,22 @@ class ProviderElementProxy /// ``` /// /// This API is not meant for public consumption. - const ProviderElementProxy(this.provider, this._lense); + const ProviderElementProxy( + this.provider, + this._lense, { + this.flushElement = false, + }); + + final bool flushElement; @override - final ProviderBase provider; + final ProviderBase provider; final ProxyElementValueListenable Function( - ProviderElement element, + ProviderElement element, ) _lense; @override - ProviderSubscriptionWithOrigin addListener( + ProviderSubscriptionWithOrigin addListener( Node source, void Function(OutT? previous, OutT next) listener, { required void Function(Object error, StackTrace stackTrace)? onError, @@ -120,24 +150,23 @@ class ProviderElementProxy onDependencyMayHaveChanged: onDependencyMayHaveChanged, ); - return _ProxySubscription( - innerSub, - removeListener, - () => _read(source), - ); - } + return ProviderSubscriptionView( + innerSubscription: innerSub, + onClose: removeListener, + read: () { + final element = source.readProviderElement(provider); + element.flush(); + element.mayNeedDispose(); - OutT _read(Node node) { - final element = node.readProviderElement(provider); - element.flush(); - element.mayNeedDispose(); - - return _lense(element).value; + return _lense(element).value; + }, + ); } @override bool operator ==(Object other) => - other is ProviderElementProxy && other.provider == provider; + other is ProviderElementProxy && + other.provider == provider; @override int get hashCode => provider.hashCode; diff --git a/packages/riverpod/lib/src/framework.dart b/packages/riverpod/lib/src/framework.dart index 1e1f7d950..7a935e78b 100644 --- a/packages/riverpod/lib/src/framework.dart +++ b/packages/riverpod/lib/src/framework.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart' as test; import 'common/env.dart'; import 'common/pragma.dart'; import 'internals.dart'; +import 'mutation.dart'; part 'core/modifiers/select_async.dart'; part 'core/provider/provider.dart'; diff --git a/packages/riverpod/lib/src/mutation.dart b/packages/riverpod/lib/src/mutation.dart new file mode 100644 index 000000000..2e922953c --- /dev/null +++ b/packages/riverpod/lib/src/mutation.dart @@ -0,0 +1,513 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:meta/meta_meta.dart'; + +import 'internals.dart'; + +// Mutation code. This should be in riverpod_annotation, but has to be here +// for the sake of ProviderObserver. + +@internal +const mutationZoneKey = #_mutation; + +/// {@template mutation} +/// Declares a method of a notifier as a "mutation". +/// +/// ## What is a mutation? +/// +/// A mutation is a method that modifies the state of a [Notifier]. +/// For example, an `addTodo` mutation would add a new todo to a todo-list. +/// +/// The primary purpose of mutations is to enable Flutter's Widgets to listen +/// to the progress of an operation. +/// Specifically, by using mutations, a widget may: +/// - Disabling a button while the operation is in progress +/// - Show a button to start an operation +/// - Show a loading indicator when the mutation is in progress. +/// - Show a snackbar when the operation completes/fails +/// +/// Mutations also make it easier to separate the logic for "starting an operation" +/// from the logic for "handling the result of an operation". +/// Two widgets can rely on the same mutation, and the progress of the operation +/// will be shared between them. +/// This way, one widget can be responsible for showing a button, while another +/// widget can be responsible for showing a loading indicator. +/// +/// Although there are ways to handle such cases without mutations, using +/// mutations makes it simpler to deal with. +/// For example, there is no need to catch possible exceptions to allow the UI +/// to show an error message. By using mutations, Riverpod will automatically +/// take care of that. +/// +/// ## How to define a mutation +/// +/// To define a mutation, we must first define a [Notifier]. +/// For that, we need to define a class annotation by `@riverpod`, and that defines +/// a `build` method: +/// +/// ```dart +/// @riverpod +/// class TodoListNotifier extends $ExampleNotifier { +/// @override +/// Future> build() { +/// /* fetch the list of todos from your server here */ +/// } +/// } +/// ``` +/// +/// Once we have defined a notifier, we can add a mutation to it. +/// To do so, we define a method in the notifier class and annotate it with [Mutation]: +/// +/// **Note**: +/// That method **must** return a [Future] that resolves with a value of the same +/// type as the notifier's state. +/// In our above example, the state of our notifier is `List`, so the mutation +/// must return a `Future>`. +/// +/// ```dart +/// @riverpod +/// class TodoListNotifier extends $ExampleNotifier { +/// /* ... */ +/// +/// @mutation +/// Future> addTodo(Todo todo) async { +/// /* to-do: Make an HTTP post request to notify the server about the added todo */ +/// +/// // Mutations are expected to return the new state for our notifier. +/// // Riverpod will then assign this value to `this.state` +/// return [...await future, todo]; +/// } +/// } +/// ``` +/// +/// ## How to use mutations +/// +/// Now that we've defined a mutation, we need a way to invoke it from our UI. +/// +/// The way this is typically done is by using `ref.watch`/`ref.listen` to obtain +/// an object that enables us to interact with the mutation: +/// +/// ```dart +/// class AddTodoButton extends ConsumerWidget { +/// @override +/// Widget build(BuildContext context, WidgetRef ref) { +/// // We use `ref.watch` to obtain the mutation object. +/// // For every `@mutation` defined in our notifier, a corresponding +/// // `myProvider.myMutation` will be available ; which can be as followed: +/// final addTodo = ref.watch(todoListProvider.addTodo); +/// } +/// } +/// ``` +/// +/// Once we have obtained the mutation object, we have two main ways to use it: +/// +/// ### 1. Start the operation inside a button press +/// +/// Mutation objects are "callable". This means that we can call them like a function. +/// When we call a mutation, it will start the operation. +/// Of course, we will have to pass the required arguments that are expected by our mutation. +/// +/// The following code shows a button that, when pressed, will start the `addTodo` mutation: +/// +/// ```dart +/// final addTodo = ref.watch(todoListProvider.addTodo); +/// +/// return ElevatedButton( +/// // Pressing the button will call `TodoListNotifier.addTodo` +/// onPressed: () => addTodo(Todo('Buy milk')), +/// ); +/// ``` +/// +/// ### 2. Listen to the progress of the operation +/// +/// Alternatively, we can use the mutation object to track the progress of the operation. +/// This is useful for many reasons, including: +/// - Disabling a button while the operation is in progress +/// - Showing a loading indicator while the operation is pending +/// - Showing a snackbar or the button in red/green when the operation fails/completes +/// +/// {@macro mutation_states} +/// +/// You can switch over the different types using pattern matching: +/// +/// ```dart +/// final addTodo = ref.watch(todoListProvider.addTodo); +/// +/// switch (addTodo.state) { +/// case IdleMutationState(): +/// print('The mutation is idle'); +/// case PendingMutationState(): +/// print('The mutation is in progress'); +/// case ErrorMutationState(:final error): +/// print('The mutation has failed with $error'); +/// case SuccessMutationState(:final value): +/// print('The mutation has succeeded, and the new state is $value'); +/// } +/// ``` +/// +/// ### Example: Showing a loading indicator while the mutation is in progress +/// +/// You can check for the [PendingMutationState] to show a loading indicator. +/// The following code shows a loading indicator while the mutation is in progress. +/// The indicator will disappear when the mutation completes or fails. +/// +/// ```dart +/// class TodoListView extends ConsumerWidget { +/// @override +/// Widget build(BuildContext context, WidgetRef ref) { +/// final addTodo = ref.watch(todoListProvider.addTodo); +/// +/// return Scaffold( +/// body: Column( +/// children: [ +/// // If the mutation is in progress, show a loading indicator +/// if (addTodo.state is PendingMutationState) +/// const LinearProgressIndicator(), +/// // See above for how AddTodoButton is defined +/// AddTodoButton(), +/// ], +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// Notice how the code that handles the loading state of our mutation +/// is separated from the code that starts the mutation. +/// In this example, even though `AddTodoButton` and `TodoListView` are separate, +/// both share the progress of the operation. This allows to easily separate +/// split the responsibilities of our widgets. +/// +/// ### Example: Showing a snackbar when the mutation completes/fails +/// +/// You can check for the [ErrorMutationState] and [SuccessMutationState] to show a snackbar. +/// +/// Since showing snackbars is done using `showSnackBar`, which is not a widget, +/// we cannot rely on `ref.watch` here. +/// Instead, we should use `ref.listen` to listen to the mutation state. +/// This will give us a callback where we can safely show a snackbar. +/// +/// ```dart +/// class TodoListView extends ConsumerWidget { +/// @override +/// Widget build(BuildContext context, WidgetRef ref) { +/// ref.listen(todoListProvider.addTodo, (_, addTodo) { +/// // We determine if the mutation succeeded or failed, and change +/// // the message accordingly. +/// String message; +/// if (addTodo.state is ErrorMutationState) { +/// message = 'Failed to add todo'; +/// } +/// else if (addTodo.state is SuccessMutationState) { +/// message = 'Todo added successfully'; +/// } +/// // We are neither in a success nor in an error state, so we do not show a snackbar. +/// else return; +/// +/// // We show a snackbar with the message. +/// ScaffoldMessenger.of(context).showSnackBar( +/// SnackBar(content: Text(message)), +/// ); +/// }); +/// } +/// } +/// ``` +/// +/// ## Example: Disabling a button while the operation is pending +/// +/// You can check for the [PendingMutationState] to know if an operation is +/// in progress. We can use this information to disable a button, by setting its +/// `onPressed` to `null`: +/// +/// ```dart +/// class AddTodoButton extends ConsumerWidget { +/// @override +/// Widget build(BuildContext context, WidgetRef ref) { +/// final addTodo = ref.watch(todoListProvider.addTodo); +/// +/// return ElevatedButton( +/// onPressed: addTodo.state is PendingMutationState +/// ? null // If the mutation is in progress, disable the button +/// : () => addTodo(Todo('Buy milk')), // Otherwise enable the button +/// ); +/// } +/// } +/// ``` +/// +/// A similar logic can be used for showing the button in red/green when the +/// operation fails/completes, by instead checking for [ErrorMutationState] and +/// [SuccessMutationState]. +/// +/// {@macro auto_reset} +/// {@endtemplate} +@Target({TargetKind.method}) +final class Mutation { + /// {@macro mutation} + const Mutation(); +} + +/// {@macro mutation} +const mutation = Mutation(); + +/// The current state of a mutation. +/// +/// {@template mutation_states} +/// A mutation can be in any of the following states: +/// - [IdleMutationState]: The mutation is not running. This is the default state. +/// - [PendingMutationState]: The mutation has been called and is in progress. +/// - [ErrorMutationState]: The mutation has failed with an error. +/// - [SuccessMutationState]: The mutation has completed successfully. +/// {@endtemplate} +sealed class MutationState { + const MutationState._(); +} + +/// The mutation is not running. +/// +/// This is the default state of a mutation. +/// A mutation can be reset to this state by calling [MutationBase.reset]. +/// +/// {@macro auto_reset} +/// +/// {@macro mutation_states} +final class IdleMutationState extends MutationState { + const IdleMutationState._() : super._(); + + @override + String toString() => 'IdleMutationState<$ResultT>()'; +} + +/// The mutation has been called and is in progress. +/// +/// {@macro mutation_states} +final class PendingMutationState extends MutationState { + const PendingMutationState._() : super._(); + + @override + String toString() => 'PendingMutationState<$ResultT>()'; +} + +/// The mutation has failed with an error. +/// +/// {@macro mutation_states} +final class ErrorMutationState extends MutationState { + ErrorMutationState._(this.error, this.stackTrace) : super._(); + + /// The error thrown by the mutation. + final Object error; + + /// The stack trace of the [error]. + final StackTrace stackTrace; + + @override + String toString() => 'ErrorMutationState<$ResultT>($error, $stackTrace)'; +} + +/// The mutation has completed successfully. +/// +/// {@macro mutation_states} +final class SuccessMutationState extends MutationState { + SuccessMutationState._(this.value) : super._(); + + /// The new state of the notifier after the mutation has completed. + final ResultT value; + + @override + String toString() => 'SuccessMutationState<$ResultT>($value)'; +} + +/// A base class that all mutations extends. +/// +/// See also [Mutation] for information on how to define a mutation. +@immutable +abstract class MutationBase { + /// The current state of the mutation. + /// + /// This defaults to [IdleMutationState]. + /// When the mutation starts, it will change to [PendingMutationState] and + /// then to either [ErrorMutationState] or [SuccessMutationState]. + /// + /// **Note**: + /// This property is immutable. The state will not change unless you + /// call `ref.watch(provider.myMutation)` again. + MutationState get state; + + /// Sets [state] back to [IdleMutationState]. + /// + /// Calling [reset] is useful when the mutation is actively listened to, + /// and you want to forcibly go back to the [IdleMutationState]. + /// + /// {@template auto_reset} + /// ## Automatic resets + /// + /// By default, mutations are automatically reset when they are no longer + /// being listened to. + /// This is similar to Riverpod's "auto-dispose" feature, for mutations. + /// If you remove all `watch`/`listen` calls to a mutation, the mutation + /// will automatically go-back to its [IdleMutationState]. + /// + /// If your mutation is always listened, you may want to call [reset] manually + /// to restore the mutation to its [IdleMutationState]. + /// {@endtemplate} + void reset(); +} + +@internal +abstract class $SyncMutationBase< + StateT, + MutationT extends $SyncMutationBase, + ClassT extends NotifierBase> + extends _MutationBase { + $SyncMutationBase({super.state, super.key}); + + @override + void setData(StateT value) { + element.setStateResult(Result.data(value)); + } +} + +@internal +abstract class $AsyncMutationBase< + StateT, + MutationT extends $AsyncMutationBase, + ClassT extends NotifierBase, Object?>> + extends _MutationBase, MutationT, ClassT> { + $AsyncMutationBase({super.state, super.key}); + + @override + void setData(StateT value) { + element.setStateResult(Result.data(AsyncData(value))); + } +} + +abstract class _MutationBase< + ValueT, + StateT, + MutationT extends _MutationBase, + ClassT extends NotifierBase> + implements MutationBase { + _MutationBase({MutationState? state, this.key}) + : state = state ?? IdleMutationState._() { + listenable.onCancel = _scheduleAutoReset; + } + + @override + final MutationState state; + final Object? key; + + ClassProviderElement get element; + ProxyElementValueListenable get listenable; + + Object? get _currentKey => listenable.result?.stateOrNull?.key; + + MutationT copyWith(MutationState state, {Object? key}); + + void setData(ValueT value); + + void _scheduleAutoReset() { + Future.microtask(() { + if (listenable.hasListeners) return; + + reset(); + }); + } + + @override + void reset() { + if (state is IdleMutationState) return; + + listenable.result = ResultData(copyWith(IdleMutationState._())); + + final context = ProviderObserverContext(element.origin, element.container); + + _notifyObserver((obs) => obs.mutationReset(context)); + } + + void _notifyObserver(void Function(ProviderObserver obs) cb) { + for (final observer in element.container.observers) { + runUnaryGuarded(cb, observer); + } + } + + void _setState(MutationContext? mutationContext, MutationT mutation) { + listenable.result = Result.data(mutation); + + final obsContext = ProviderObserverContext( + element.origin, + element.container, + mutation: mutationContext, + ); + + switch (mutation.state) { + case ErrorMutationState(:final error, :final stackTrace): + _notifyObserver( + (obs) => obs.mutationError( + obsContext, + mutationContext!, + error, + stackTrace, + ), + ); + + case SuccessMutationState(:final value): + _notifyObserver( + (obs) => obs.mutationSuccess(obsContext, mutationContext!, value), + ); + + case PendingMutationState(): + _notifyObserver( + (obs) => obs.mutationStart(obsContext, mutationContext!), + ); + + default: + } + } + + @protected + Future mutateAsync( + Invocation invocation, + FutureOr Function(ClassT clazz) cb, + ) { + element.flush(); + final notifier = element.classListenable.value; + final mutationContext = MutationContext(invocation, notifier); + + return runZoned( + zoneValues: {mutationZoneKey: mutationContext}, + () async { + // ! is safe because of the flush() above + final key = Object(); + try { + _setState( + mutationContext, + copyWith(PendingMutationState._(), key: key), + ); + + final result = await cb(notifier); + if (key == _currentKey) { + _setState( + mutationContext, + copyWith(SuccessMutationState._(result)), + ); + } + setData(result); + + return result; + } catch (err, stack) { + if (key == _currentKey) { + _setState( + mutationContext, + copyWith(ErrorMutationState._(err, stack)), + ); + } + + rethrow; + } + }, + ); + } + + @override + String toString() => '$runtimeType#${shortHash(this)}($state)'; +} diff --git a/packages/riverpod/lib/src/providers/legacy/state_notifier_provider.dart b/packages/riverpod/lib/src/providers/legacy/state_notifier_provider.dart index 60f8bd8ae..a46f22601 100644 --- a/packages/riverpod/lib/src/providers/legacy/state_notifier_provider.dart +++ b/packages/riverpod/lib/src/providers/legacy/state_notifier_provider.dart @@ -4,11 +4,11 @@ import 'package:state_notifier/state_notifier.dart'; import '../../builder.dart'; import '../../internals.dart'; -ProviderElementProxy +ProviderElementProxy _notifier, StateT>( StateNotifierProvider that, ) { - return ProviderElementProxy( + return ProviderElementProxy( that, (element) { return (element as StateNotifierProviderElement) diff --git a/packages/riverpod/lib/src/providers/legacy/state_provider.dart b/packages/riverpod/lib/src/providers/legacy/state_provider.dart index ed2ca1c31..1d2006d7f 100644 --- a/packages/riverpod/lib/src/providers/legacy/state_provider.dart +++ b/packages/riverpod/lib/src/providers/legacy/state_provider.dart @@ -4,10 +4,10 @@ import '../../builder.dart'; import '../../internals.dart'; import 'state_controller.dart'; -ProviderElementProxy> _notifier( +ProviderElementProxy, StateT> _notifier( StateProvider that, ) { - return ProviderElementProxy>( + return ProviderElementProxy, StateT>( that, (element) { return (element as StateProviderElement)._controllerNotifier; diff --git a/packages/riverpod/test/old/utils.dart b/packages/riverpod/test/old/utils.dart index f7839184e..6f8965cf0 100644 --- a/packages/riverpod/test/old/utils.dart +++ b/packages/riverpod/test/old/utils.dart @@ -182,56 +182,82 @@ class ObserverMock extends Mock implements ProviderObserver { } @override - void didDisposeProvider( - ProviderBase? provider, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method(#didDisposeProvider, [provider, container]), - ); - } + void didAddProvider( + ProviderObserverContext? context, + Object? value, + ); @override void providerDidFail( - ProviderBase? provider, + ProviderObserverContext? context, Object? error, - Object? stackTrace, - Object? container, - ) { - super.noSuchMethod( - Invocation.method( - #providerDidFail, - [provider, error, stackTrace, container], - ), - ); - } - - @override - void didAddProvider( - ProviderBase? provider, - Object? value, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method(#didAddProvider, [provider, value, container]), - ); - } + StackTrace? stackTrace, + ); @override void didUpdateProvider( - ProviderBase? provider, + ProviderObserverContext? context, Object? previousValue, Object? newValue, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method( - #didUpdateProvider, - [provider, previousValue, newValue, container], - ), - ); - } + ); + + @override + void didDisposeProvider(ProviderObserverContext? context); + + @override + void mutationReset(ProviderObserverContext? context); + + @override + void mutationStart( + ProviderObserverContext? context, + MutationContext? mutation, + ); + + @override + void mutationError( + ProviderObserverContext? context, + MutationContext? mutation, + Object? error, + StackTrace? stackTrace, + ); + + @override + void mutationSuccess( + ProviderObserverContext? context, + MutationContext? mutation, + Object? result, + ); } // can subclass ProviderObserver without implementing all life-cycles class CustomObserver extends ProviderObserver {} + +TypeMatcher isProviderObserverContext( + Object? provider, + Object? container, { + Object? mutation, +}) { + var matcher = isA(); + + matcher = matcher.having((e) => e.provider, 'provider', provider); + matcher = matcher.having((e) => e.container, 'container', container); + if (mutation != null) { + matcher = matcher.having((e) => e.mutation, 'mutation', mutation); + } + + return matcher; +} + +TypeMatcher isMutationContext( + Object? invocation, { + Object? notifier, +}) { + var matcher = isA(); + + matcher = matcher.having((e) => e.invocation, 'invocation', invocation); + if (notifier != null) { + matcher = matcher.having((e) => e.notifier, 'notifier', notifier); + } + + return matcher; +} diff --git a/packages/riverpod/test/src/core/provider_element_test.dart b/packages/riverpod/test/src/core/provider_element_test.dart index 070019fde..c72be45fd 100644 --- a/packages/riverpod/test/src/core/provider_element_test.dart +++ b/packages/riverpod/test/src/core/provider_element_test.dart @@ -25,12 +25,12 @@ void main() { expect(providerElement.subscriptions, null); expect( providerElement.dependents, - [isA, int>>()], + [isA>()], ); expect(providerElement.weakDependents, isEmpty); expect(depElement.subscriptions, [ - isA, int>>(), + isA>(), ]); expect(depElement.dependents, isEmpty); expect(depElement.weakDependents, isEmpty); diff --git a/packages/riverpod/test/src/core/provider_observer_test.dart b/packages/riverpod/test/src/core/provider_observer_test.dart index 609ed9a4b..ce00b940e 100644 --- a/packages/riverpod/test/src/core/provider_observer_test.dart +++ b/packages/riverpod/test/src/core/provider_observer_test.dart @@ -9,18 +9,6 @@ import '../utils.dart'; void main() { group('ProviderObserver', () { - test('life-cycles do nothing by default', () { - const observer = ConstObserver(); - - final provider = Provider((ref) => 0); - final container = ProviderContainer.test(); - - observer.didAddProvider(provider, 0, container); - observer.didDisposeProvider(provider, container); - observer.didUpdateProvider(provider, 0, 0, container); - observer.providerDidFail(provider, 0, StackTrace.empty, container); - }); - test('can have const constructors', () { final root = ProviderContainer.test( observers: [ @@ -36,8 +24,9 @@ void main() { () async { final observer = ObserverMock(); final observer2 = ObserverMock(); - final container = - ProviderContainer.test(observers: [observer, observer2]); + final container = ProviderContainer.test( + observers: [observer, observer2], + ); final dep = StateProvider((ref) => 0); final provider = Provider((ref) { if (ref.watch(dep) == 0) { @@ -56,19 +45,17 @@ void main() { verifyInOrder([ observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), null, 0, - container, ), observer2.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), null, 0, - container, ), ]); - verifyNever(observer.providerDidFail(any, any, any, any)); + verifyNever(observer.providerDidFail(any, any, any)); }); test( @@ -102,13 +89,37 @@ void main() { expect(child.read(provider.notifier).state++, 42); - verify(observer.didUpdateProvider(provider, 42, 43, child)).called(1); - verify(observer2.didUpdateProvider(provider, 42, 43, child)).called(1); - verify(observer3.didUpdateProvider(provider, 42, 43, child)).called(1); + verify( + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + 43, + ), + ).called(1); + verify( + observer2.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + 43, + ), + ).called(1); + verify( + observer3.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + 43, + ), + ).called(1); mid.read(provider.notifier).state++; - verify(observer.didUpdateProvider(provider, 0, 1, root)).called(1); + verify( + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, root)), + 0, + 1, + ), + ).called(1); verifyNoMoreInteractions(observer3); verifyNoMoreInteractions(observer2); @@ -131,7 +142,11 @@ void main() { verifyOnly( observer, - observer.didUpdateProvider(computed, 0, 1, container), + observer.didUpdateProvider( + argThat(isProviderObserverContext(computed, container)), + 0, + 1, + ), ).called(1); }); @@ -148,7 +163,11 @@ void main() { verifyOnly( observer, - observer.didUpdateProvider(provider, 0, 1, container), + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), ).called(1); }); @@ -170,8 +189,14 @@ void main() { verify(listener(null, 0)).called(1); verifyNoMoreInteractions(listener); verifyInOrder([ - observer.didAddProvider(provider, 0, container), - observer2.didAddProvider(provider, 0, container), + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + ), + observer2.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + ), ]); verifyNoMoreInteractions(observer); verifyNoMoreInteractions(observer2); @@ -180,8 +205,16 @@ void main() { verifyInOrder([ listener(0, 1), - observer.didUpdateProvider(provider, 0, 1, container), - observer2.didUpdateProvider(provider, 0, 1, container), + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), + observer2.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), ]); verifyNoMoreInteractions(listener); verifyNoMoreInteractions(observer); @@ -190,11 +223,9 @@ void main() { test('guards didUpdateProviders', () { final observer = ObserverMock(); - when(observer.didUpdateProvider(any, any, any, any)) - .thenThrow('error1'); + when(observer.didUpdateProvider(any, any, any)).thenThrow('error1'); final observer2 = ObserverMock(); - when(observer2.didUpdateProvider(any, any, any, any)) - .thenThrow('error2'); + when(observer2.didUpdateProvider(any, any, any)).thenThrow('error2'); final observer3 = ObserverMock(); final provider = StateNotifierProvider((_) => Counter()); final counter = Counter(); @@ -214,9 +245,21 @@ void main() { expect(errors, ['error1', 'error2']); verifyInOrder([ - observer.didUpdateProvider(provider, 0, 1, container), - observer2.didUpdateProvider(provider, 0, 1, container), - observer3.didUpdateProvider(provider, 0, 1, container), + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), + observer2.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), + observer3.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 1, + ), ]); verifyNoMoreInteractions(observer); verifyNoMoreInteractions(observer2); @@ -247,12 +290,13 @@ void main() { await container.pump(); verifyInOrder([ - observer.didDisposeProvider(isNegative, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(isNegative, container)), + ), observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 0, 1, - container, ), ]); verifyNoMoreInteractions(observer); @@ -261,19 +305,19 @@ void main() { await container.pump(); verifyInOrder([ - observer.didDisposeProvider(isNegative, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(isNegative, container)), + ), observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 1, -10, - container, ), isNegativeListener(false, true), observer.didUpdateProvider( - isNegative, + argThat(isProviderObserverContext(isNegative, container)), false, true, - container, ), ]); verifyNoMoreInteractions(isNegativeListener); @@ -325,20 +369,54 @@ void main() { await child.pump(); verifyInOrder([ - observer.didDisposeProvider(provider, child), - observer.didUpdateProvider(dep, 0, 1, root), - observer.didUpdateProvider(provider, 42, null, child), - observer.providerDidFail(provider, 'error', StackTrace.empty, child), + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, child)), + ), + observer.didUpdateProvider( + argThat(isProviderObserverContext(dep, root)), + 0, + 1, + ), + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + null, + ), + observer.providerDidFail( + argThat(isProviderObserverContext(provider, child)), + 'error', + StackTrace.empty, + ), ]); verifyInOrder([ - observer2.didDisposeProvider(provider, child), - observer2.didUpdateProvider(provider, 42, null, child), - observer2.providerDidFail(provider, 'error', StackTrace.empty, child), + observer2.didDisposeProvider( + argThat(isProviderObserverContext(provider, child)), + ), + observer2.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + null, + ), + observer2.providerDidFail( + argThat(isProviderObserverContext(provider, child)), + 'error', + StackTrace.empty, + ), ]); verifyInOrder([ - observer3.didDisposeProvider(provider, child), - observer3.didUpdateProvider(provider, 42, null, child), - observer3.providerDidFail(provider, 'error', StackTrace.empty, child), + observer3.didDisposeProvider( + argThat(isProviderObserverContext(provider, child)), + ), + observer3.didUpdateProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + null, + ), + observer3.providerDidFail( + argThat(isProviderObserverContext(provider, child)), + 'error', + StackTrace.empty, + ), ]); verifyNoMoreInteractions(observer3); @@ -358,21 +436,18 @@ void main() { verifyInOrder([ observer.didAddProvider( - provider, + argThat(isProviderObserverContext(provider, container)), const AsyncLoading(), - container, ), observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), const AsyncLoading(), const AsyncError('error', StackTrace.empty), - container, ), observer.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), 'error', StackTrace.empty, - container, ), ]); }); @@ -389,21 +464,18 @@ void main() { verifyInOrder([ observer.didAddProvider( - provider, + argThat(isProviderObserverContext(provider, container)), const AsyncLoading(), - container, ), observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), const AsyncLoading(), const AsyncError('error', StackTrace.empty), - container, ), observer.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), 'error', StackTrace.empty, - container, ), ]); }); @@ -421,19 +493,23 @@ void main() { ); verifyInOrder([ - observer.didAddProvider(provider, null, container), - observer2.didAddProvider(provider, null, container), + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + null, + ), + observer2.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + null, + ), observer.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), argThat(isUnimplementedError), argThat(isNotNull), - container, ), observer2.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), argThat(isUnimplementedError), argThat(isNotNull), - container, ), ]); verifyNoMoreInteractions(observer); @@ -462,28 +538,24 @@ void main() { verifyInOrder([ observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 0, null, - container, ), observer2.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 0, null, - container, ), observer.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), argThat(isUnimplementedError), argThat(isNotNull), - container, ), observer2.providerDidFail( - provider, + argThat(isProviderObserverContext(provider, container)), argThat(isUnimplementedError), argThat(isNotNull), - container, ), ]); }); @@ -500,7 +572,12 @@ void main() { throwsUnimplementedError, ); - verify(observer.didAddProvider(provider, null, container)); + verify( + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + null, + ), + ); }); test( @@ -527,14 +604,28 @@ void main() { expect(child.read(provider), 42); verifyInOrder([ - observer3.didAddProvider(provider, 42, child), - observer2.didAddProvider(provider, 42, child), - observer.didAddProvider(provider, 42, child), + observer3.didAddProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + ), + observer2.didAddProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + ), + observer.didAddProvider( + argThat(isProviderObserverContext(provider, child)), + 42, + ), ]); expect(mid.read(provider), 0); - verify(observer.didAddProvider(provider, 0, root)).called(1); + verify( + observer.didAddProvider( + argThat(isProviderObserverContext(provider, root)), + 0, + ), + ).called(1); verifyNoMoreInteractions(observer3); verifyNoMoreInteractions(observer2); @@ -551,14 +642,12 @@ void main() { expect(container.read(provider), 42); verifyInOrder([ observer.didAddProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 42, - container, ), observer2.didAddProvider( - provider, + argThat(isProviderObserverContext(provider, container)), 42, - container, ), ]); verifyNoMoreInteractions(observer); @@ -567,9 +656,9 @@ void main() { test('guards against exceptions', () { final observer = ObserverMock(); - when(observer.didAddProvider(any, any, any)).thenThrow('error1'); + when(observer.didAddProvider(any, any)).thenThrow('error1'); final observer2 = ObserverMock(); - when(observer2.didAddProvider(any, any, any)).thenThrow('error2'); + when(observer2.didAddProvider(any, any)).thenThrow('error2'); final observer3 = ObserverMock(); final provider = Provider((_) => 42); final container = ProviderContainer.test( @@ -587,9 +676,18 @@ void main() { expect(result, 42); expect(errors, ['error1', 'error2']); verifyInOrder([ - observer.didAddProvider(provider, 42, container), - observer2.didAddProvider(provider, 42, container), - observer3.didAddProvider(provider, 42, container), + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 42, + ), + observer2.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 42, + ), + observer3.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 42, + ), ]); verifyNoMoreInteractions(observer); }); @@ -606,7 +704,12 @@ void main() { clearInteractions(observer); container.invalidate(provider); - verifyOnly(observer, observer.didDisposeProvider(provider, container)); + verifyOnly( + observer, + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), + ); container.invalidate(provider); verifyNoMoreInteractions(observer); @@ -624,7 +727,9 @@ void main() { container.dispose(); verifyInOrder([ - observer.didDisposeProvider(provider, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), ]); verifyNoMoreInteractions(observer); }); @@ -644,16 +749,18 @@ void main() { await container.pump(); verifyInOrder([ - observer.didDisposeProvider(provider, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), ]); verifyNoMoreInteractions(observer); }); test('is guarded', () { final observer = ObserverMock(); - when(observer.didDisposeProvider(any, any)).thenThrow('error1'); + when(observer.didDisposeProvider(any)).thenThrow('error1'); final observer2 = ObserverMock(); - when(observer2.didDisposeProvider(any, any)).thenThrow('error2'); + when(observer2.didDisposeProvider(any)).thenThrow('error2'); final observer3 = ObserverMock(); final onDispose = OnDisposeMock(); final provider = Provider((ref) { @@ -677,13 +784,25 @@ void main() { expect(errors, ['error1', 'error2', 'error1', 'error2']); verifyInOrder([ - observer.didDisposeProvider(provider2, container), - observer2.didDisposeProvider(provider2, container), - observer3.didDisposeProvider(provider2, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider2, container)), + ), + observer2.didDisposeProvider( + argThat(isProviderObserverContext(provider2, container)), + ), + observer3.didDisposeProvider( + argThat(isProviderObserverContext(provider2, container)), + ), onDispose(), - observer.didDisposeProvider(provider, container), - observer2.didDisposeProvider(provider, container), - observer3.didDisposeProvider(provider, container), + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), + observer2.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), + observer3.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), ]); verifyNoMoreInteractions(onDispose); verifyNoMoreInteractions(observer); diff --git a/packages/riverpod/test/src/core/ref_test.dart b/packages/riverpod/test/src/core/ref_test.dart index d110b3e0d..f3610bfae 100644 --- a/packages/riverpod/test/src/core/ref_test.dart +++ b/packages/riverpod/test/src/core/ref_test.dart @@ -1860,7 +1860,7 @@ void main() { group('.notifyListeners', () { test('If called after initialization, notify listeners', () { - final observer = ProviderObserverMock(); + final observer = ObserverMock(); final listener = Listener(); final selfListener = Listener(); final container = ProviderContainer.test(observers: [observer]); @@ -1873,7 +1873,13 @@ void main() { container.listen(provider, listener.call, fireImmediately: true); - verifyOnly(observer, observer.didAddProvider(provider, 0, container)); + verifyOnly( + observer, + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + ), + ); verifyOnly(listener, listener(null, 0)); verifyOnly(selfListener, selfListener(null, 0)); @@ -1883,14 +1889,18 @@ void main() { verifyOnly(selfListener, selfListener(0, 0)); verifyOnly( observer, - observer.didUpdateProvider(provider, 0, 0, container), + observer.didUpdateProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + 0, + ), ); }); test( 'can be invoked during first initialization, and does not notify listeners', () { - final observer = ProviderObserverMock(); + final observer = ObserverMock(); final selfListener = Listener(); final listener = Listener(); final container = ProviderContainer.test(observers: [observer]); @@ -1905,7 +1915,13 @@ void main() { container.listen(provider, listener.call, fireImmediately: true); - verifyOnly(observer, observer.didAddProvider(provider, 0, container)); + verifyOnly( + observer, + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + 0, + ), + ); verifyOnly(listener, listener(null, 0)); verifyOnly(selfListener, selfListener(null, 0)); }); @@ -1913,7 +1929,7 @@ void main() { test( 'can be invoked during a re-initialization, and does not notify listeners', () { - final observer = ProviderObserverMock(); + final observer = ObserverMock(); final listener = Listener(); final selfListener = Listener(); final container = ProviderContainer.test(observers: [observer]); @@ -1936,7 +1952,10 @@ void main() { verifyOnly( observer, - observer.didAddProvider(provider, firstValue, container), + observer.didAddProvider( + argThat(isProviderObserverContext(provider, container)), + firstValue, + ), ); verifyOnly(selfListener, selfListener(null, firstValue)); verifyOnly(listener, listener(null, firstValue)); @@ -1947,13 +1966,16 @@ void main() { verifyOnly(selfListener, selfListener(firstValue, secondValue)); verifyOnly(listener, listener(firstValue, secondValue)); - verify(observer.didDisposeProvider(provider, container)); + verify( + observer.didDisposeProvider( + argThat(isProviderObserverContext(provider, container)), + ), + ); verify( observer.didUpdateProvider( - provider, + argThat(isProviderObserverContext(provider, container)), firstValue, secondValue, - container, ), ).called(1); verifyNoMoreInteractions(observer); diff --git a/packages/riverpod/test/src/utils.dart b/packages/riverpod/test/src/utils.dart index d2344bcf3..5321322b3 100644 --- a/packages/riverpod/test/src/utils.dart +++ b/packages/riverpod/test/src/utils.dart @@ -5,6 +5,9 @@ import 'package:riverpod/legacy.dart'; import 'package:riverpod/src/internals.dart'; import 'package:test/test.dart' hide Retry; +export '../old/utils.dart' + show ObserverMock, isProviderObserverContext, isMutationContext; + List captureErrors(List cb) { final errors = []; for (final fn in cb) { @@ -18,8 +21,6 @@ List captureErrors(List cb) { return errors; } -class ProviderObserverMock extends Mock implements ProviderObserver {} - class StreamSubscriptionView implements StreamSubscription { StreamSubscriptionView(this.inner); @@ -249,65 +250,3 @@ List errorsOf(void Function() cb) { runZonedGuarded(cb, (err, _) => errors.add(err)); return [...errors]; } - -class ObserverMock extends Mock implements ProviderObserver { - ObserverMock([this.label]); - - final String? label; - - @override - String toString() { - return label ?? super.toString(); - } - - @override - void didDisposeProvider( - ProviderBase? provider, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method(#didDisposeProvider, [provider, container]), - ); - } - - @override - void providerDidFail( - ProviderBase? provider, - Object? error, - Object? stackTrace, - Object? container, - ) { - super.noSuchMethod( - Invocation.method( - #providerDidFail, - [provider, error, stackTrace, container], - ), - ); - } - - @override - void didAddProvider( - ProviderBase? provider, - Object? value, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method(#didAddProvider, [provider, value, container]), - ); - } - - @override - void didUpdateProvider( - ProviderBase? provider, - Object? previousValue, - Object? newValue, - ProviderContainer? container, - ) { - super.noSuchMethod( - Invocation.method( - #didUpdateProvider, - [provider, previousValue, newValue, container], - ), - ); - } -} diff --git a/packages/riverpod_analyzer_utils/CHANGELOG.md b/packages/riverpod_analyzer_utils/CHANGELOG.md index 2836f63f1..129fe7e40 100644 --- a/packages/riverpod_analyzer_utils/CHANGELOG.md +++ b/packages/riverpod_analyzer_utils/CHANGELOG.md @@ -3,6 +3,7 @@ - **Breaking**: Rewrote all RiverpodAst nodes to instead be extensions on `AstNodes`. Too many changes to detail everything. I'm the only one who uses this package anyway. If you're reading this, have a nice day! +- Added support for parsing `@mutation` ## 1.0.0-dev.1 - 2023-11-20 diff --git a/packages/riverpod_analyzer_utils/lib/src/errors.dart b/packages/riverpod_analyzer_utils/lib/src/errors.dart index a8a0d99da..eed83d768 100644 --- a/packages/riverpod_analyzer_utils/lib/src/errors.dart +++ b/packages/riverpod_analyzer_utils/lib/src/errors.dart @@ -19,6 +19,9 @@ enum RiverpodAnalysisErrorCode { providerDependencyListParseError, providerOrFamilyExpressionParseError, invalidRetryArgument, + mutationReturnTypeMismatch, + mutationIsStatic, + mutationIsAbstract, } class RiverpodAnalysisError { diff --git a/packages/riverpod_analyzer_utils/lib/src/nodes.dart b/packages/riverpod_analyzer_utils/lib/src/nodes.dart index fe288f180..753e7d115 100644 --- a/packages/riverpod_analyzer_utils/lib/src/nodes.dart +++ b/packages/riverpod_analyzer_utils/lib/src/nodes.dart @@ -9,6 +9,7 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; @@ -34,7 +35,7 @@ part 'nodes/providers/identifiers.dart'; part 'nodes/provider_for.dart'; part 'nodes/provider_or_family.dart'; -part 'nodes/riverpod.dart'; +part 'nodes/annotation.dart'; part 'nodes/provider_listenable.dart'; part 'nodes/ref_invocation.dart'; part 'nodes/widget_ref_invocation.dart'; diff --git a/packages/riverpod_analyzer_utils/lib/src/nodes/riverpod.dart b/packages/riverpod_analyzer_utils/lib/src/nodes/annotation.dart similarity index 68% rename from packages/riverpod_analyzer_utils/lib/src/nodes/riverpod.dart rename to packages/riverpod_analyzer_utils/lib/src/nodes/annotation.dart index 2e71ac507..3420d9360 100644 --- a/packages/riverpod_analyzer_utils/lib/src/nodes/riverpod.dart +++ b/packages/riverpod_analyzer_utils/lib/src/nodes/annotation.dart @@ -131,3 +131,72 @@ final class RiverpodAnnotationElement { final List? dependencies; final Set? allTransitiveDependencies; } + +extension MutationAnnotatedNodeOfX on AnnotatedNode { + static final _cache = Expando>(); + + MutationAnnotation? get mutation { + return _cache.upsert(this, () { + return metadata.map((e) => e.mutation).nonNulls.firstOrNull; + }); + } +} + +extension MutationAnnotationX on Annotation { + static final _cache = Expando>(); + + MutationAnnotation? get mutation { + return _cache.upsert(this, () { + final elementAnnotation = this.elementAnnotation; + final element = this.element; + if (element == null || elementAnnotation == null) return null; + if (element is! ExecutableElement || + !mutationType.isExactlyType(element.returnType)) { + // The annotation is not an @Mutation + return null; + } + + final mutationAnnotationElement = MutationAnnotationElement._parse( + elementAnnotation, + ); + if (mutationAnnotationElement == null) return null; + + return MutationAnnotation._( + node: this, + element: mutationAnnotationElement, + ); + }); + } +} + +final class MutationAnnotation { + MutationAnnotation._({required this.node, required this.element}); + + final Annotation node; + final MutationAnnotationElement element; +} + +final class MutationAnnotationElement { + MutationAnnotationElement._({ + required this.element, + }); + + static final _cache = _Cache(); + + static MutationAnnotationElement? _parse(ElementAnnotation element) { + return _cache(element, () { + final type = element.element.cast()?.returnType; + if (type == null || !mutationType.isExactlyType(type)) return null; + + return MutationAnnotationElement._( + element: element, + ); + }); + } + + static MutationAnnotationElement? _of(Element element) { + return element.metadata.map(_parse).nonNulls.firstOrNull; + } + + final ElementAnnotation element; +} diff --git a/packages/riverpod_analyzer_utils/lib/src/nodes/providers/notifier.dart b/packages/riverpod_analyzer_utils/lib/src/nodes/providers/notifier.dart index 639a4b198..dfe10e6aa 100644 --- a/packages/riverpod_analyzer_utils/lib/src/nodes/providers/notifier.dart +++ b/packages/riverpod_analyzer_utils/lib/src/nodes/providers/notifier.dart @@ -108,7 +108,11 @@ final class ClassBasedProviderDeclaration extends GeneratorProviderDeclaration { required this.createdTypeNode, required this.exposedTypeNode, required this.valueTypeNode, - }); + }) : mutations = node.members + .whereType() + .map((e) => e.mutation) + .nonNulls + .toList(); @override final Token name; @@ -125,6 +129,101 @@ final class ClassBasedProviderDeclaration extends GeneratorProviderDeclaration { final TypeAnnotation? valueTypeNode; @override final SourcedType exposedTypeNode; + + final List mutations; +} + +extension MutationMethodDeclarationX on MethodDeclaration { + static final _cache = _Cache(); + + Mutation? get mutation { + return _cache(this, () { + final element = declaredElement; + if (element == null) return null; + + final mutationElement = MutationElement._parse(element); + if (mutationElement == null) return null; + + if (isStatic) { + errorReporter( + RiverpodAnalysisError( + 'Mutations cannot be static.', + targetNode: this, + targetElement: element, + code: RiverpodAnalysisErrorCode.mutationIsStatic, + ), + ); + return null; + } + if (isAbstract) { + errorReporter( + RiverpodAnalysisError( + 'Mutations cannot be abstract.', + targetNode: this, + targetElement: element, + code: RiverpodAnalysisErrorCode.mutationIsAbstract, + ), + ); + return null; + } + + final expectedReturnType = thisOrAncestorOfType()! + .members + .whereType() + .firstWhereOrNull((e) => e.name.lexeme == 'build') + ?.returnType; + if (expectedReturnType == null) return null; + + final expectedValueType = _getValueType( + expectedReturnType, + element.library, + ); + if (expectedValueType == null) return null; + + final expectedType = + element.library.typeProvider.futureOrElement.instantiate( + typeArguments: [expectedValueType.type!], + nullabilitySuffix: NullabilitySuffix.none, + ); + + final actualType = element.returnType; + + final isAssignable = element.library.typeSystem.isAssignableTo( + actualType, + expectedType, + strictCasts: true, + ); + if (!isAssignable) { + errorReporter( + RiverpodAnalysisError( + 'The return type of mutations must match the type returned by the "build" method.', + targetNode: this, + targetElement: element, + code: RiverpodAnalysisErrorCode.mutationReturnTypeMismatch, + ), + ); + return null; + } + + final mutation = Mutation._( + node: this, + element: mutationElement, + ); + + return mutation; + }); + } +} + +final class Mutation { + Mutation._({ + required this.node, + required this.element, + }); + + String get name => node.name.lexeme; + final MethodDeclaration node; + final MutationElement element; } class ClassBasedProviderDeclarationElement @@ -185,3 +284,27 @@ class ClassBasedProviderDeclarationElement final ExecutableElement buildMethod; } + +class MutationElement { + MutationElement._({ + required this.name, + required this.method, + }); + + static final _cache = _Cache(); + + static MutationElement? _parse(ExecutableElement element) { + return _cache(element, () { + final annotation = MutationAnnotationElement._of(element); + if (annotation == null) return null; + + return MutationElement._( + name: element.name, + method: element, + ); + }); + } + + final String name; + final ExecutableElement method; +} diff --git a/packages/riverpod_analyzer_utils/lib/src/riverpod_types/generator.dart b/packages/riverpod_analyzer_utils/lib/src/riverpod_types/generator.dart index cdb126a55..eeefbba62 100644 --- a/packages/riverpod_analyzer_utils/lib/src/riverpod_types/generator.dart +++ b/packages/riverpod_analyzer_utils/lib/src/riverpod_types/generator.dart @@ -17,3 +17,9 @@ const riverpodType = TypeChecker.fromName( 'Riverpod', packageName: 'riverpod_annotation', ); + +/// Matches with the `Mutation` annotation from riverpod_annotation. +const mutationType = TypeChecker.fromName( + 'Mutation', + packageName: 'riverpod', +); diff --git a/packages/riverpod_analyzer_utils_tests/test/mutation_test.dart b/packages/riverpod_analyzer_utils_tests/test/mutation_test.dart new file mode 100644 index 000000000..d9dccd451 --- /dev/null +++ b/packages/riverpod_analyzer_utils_tests/test/mutation_test.dart @@ -0,0 +1,98 @@ +import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; +import 'package:test/test.dart'; + +import 'analyzer_test_utils.dart'; + +void main() { + testSource( + 'Rejects mutations with a return value non-matching the build value', + source: r''' +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +@riverpod +class SyncNotifier extends _$SyncNotifier { + @override + T build() => throw 42; + + @mutation + Stream a() => throw 42; + + @mutation + T b() => throw 42; + + @mutation + FutureOr c() => throw 42; + + @mutation + Future d() => throw 42; + + @mutation + Future e() => throw 42; + + @mutation + int e() => throw 42; +} +''', (resolver, unit, units) async { + final result = + await resolver.resolveRiverpodAnalysisResult(ignoreErrors: true); + + expect(result.errors, hasLength(3)); + + expect( + result.errors, + everyElement( + isA() + .having((e) => e.targetNode, 'node', isNotNull) + .having((e) => e.targetElement, 'element', isNotNull) + .having( + (e) => e.code, + 'code', + RiverpodAnalysisErrorCode.mutationReturnTypeMismatch, + ) + .having( + (e) => e.message, + 'message', + 'The return type of mutations must match the type returned by the "build" method.', + ), + ), + ); + }); + + testSource('rejects abstract/static mutations', source: r''' +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +@riverpod +class Abstract extends _$Abstract { + @override + int build() => 0; + + @mutation + Future a(); + + @mutation + static Future b() async => 42; +} +''', (resolver, unit, units) async { + final result = + await resolver.resolveRiverpodAnalysisResult(ignoreErrors: true); + + expect(result.errors, hasLength(2)); + + expect( + result.errors, + everyElement( + isA() + .having((e) => e.targetNode, 'node', isNotNull) + .having((e) => e.targetElement, 'element', isNotNull) + .having( + (e) => e.code, + 'code', + anyOf( + RiverpodAnalysisErrorCode.mutationIsAbstract, + RiverpodAnalysisErrorCode.mutationIsStatic, + ), + ), + ), + ); + }); +} diff --git a/packages/riverpod_annotation/CHANGELOG.md b/packages/riverpod_annotation/CHANGELOG.md index a677b4bc5..d8f6544fc 100644 --- a/packages/riverpod_annotation/CHANGELOG.md +++ b/packages/riverpod_annotation/CHANGELOG.md @@ -3,6 +3,9 @@ - **Breaking** various `package:riverpod` objects are no-longer exported. If you wish to use providers by hand, you will have to separately import `package:riverpod/riverpod.dart`. +- Added `@mutation` support. + Mutations are a way to enable your UI to easily listen to the status of side-effects. + See the documentation of `@mutation` for further information. - Made `@Riverpod` final - Added `@Dependencies([...])`, for lint purposes. This is similar to `@Riverpod(dependencies: [...])`, but is applied on diff --git a/packages/riverpod_annotation/analysis_options.yaml b/packages/riverpod_annotation/analysis_options.yaml new file mode 100644 index 000000000..a9a268589 --- /dev/null +++ b/packages/riverpod_annotation/analysis_options.yaml @@ -0,0 +1,6 @@ +include: ../../analysis_options.yaml +analyzer: + errors: + # We have a tight constraint on Riverpod to use its internal APIs + invalid_use_of_internal_member: ignore + implementation_imports: ignore diff --git a/packages/riverpod_annotation/lib/riverpod_annotation.dart b/packages/riverpod_annotation/lib/riverpod_annotation.dart index feb355c4a..d8ddb66ab 100644 --- a/packages/riverpod_annotation/lib/riverpod_annotation.dart +++ b/packages/riverpod_annotation/lib/riverpod_annotation.dart @@ -58,6 +58,13 @@ export 'package:riverpod/src/internals.dart' $NotifierProviderElement, $Notifier; +// ignore: invalid_export_of_internal_element, used by the generator. +export 'package:riverpod/src/mutation.dart' + show $SyncMutationBase, $AsyncMutationBase; +// Separate export to avoid silencing valid @internal issues +export 'package:riverpod/src/mutation.dart' + hide $SyncMutationBase, $AsyncMutationBase; + export 'src/riverpod_annotation.dart'; /// An implementation detail of `riverpod_generator`. diff --git a/packages/riverpod_generator/CHANGELOG.md b/packages/riverpod_generator/CHANGELOG.md index 348a2e2a1..3045f40b8 100644 --- a/packages/riverpod_generator/CHANGELOG.md +++ b/packages/riverpod_generator/CHANGELOG.md @@ -14,7 +14,7 @@ } ) ``` - +- Added support for mutations. See also `@mutation` for further information. - Added support for `@Riverpod(retry: ...)` ## 2.6.3 - 2024-11-18 diff --git a/packages/riverpod_generator/lib/src/riverpod_generator.dart b/packages/riverpod_generator/lib/src/riverpod_generator.dart index d3e1e6108..abbae9ba9 100644 --- a/packages/riverpod_generator/lib/src/riverpod_generator.dart +++ b/packages/riverpod_generator/lib/src/riverpod_generator.dart @@ -10,8 +10,10 @@ import 'package:source_gen/source_gen.dart'; import 'models.dart'; import 'parse_generator.dart'; +import 'templates/element.dart'; import 'templates/family.dart'; import 'templates/hash.dart'; +import 'templates/mutation.dart'; import 'templates/notifier.dart'; import 'templates/parameters.dart'; import 'templates/provider.dart'; @@ -202,6 +204,10 @@ class _RiverpodGeneratorVisitor { ) { visitGeneratorProviderDeclaration(provider); NotifierTemplate(provider).run(buffer); + ElementTemplate(provider).run(buffer); + for (final mutation in provider.mutations) { + MutationTemplate(mutation, provider).run(buffer); + } } void visitFunctionalProviderDeclaration( @@ -343,9 +349,9 @@ extension ProviderNames on GeneratorProviderDeclaration { final ClassBasedProviderDeclaration p => p.node.typeParameters }; - String generics() => _genericUsageDisplayString(typeParameters); + String generics() => typeParameters.genericUsageDisplayString(); String genericsDefinition() => - _genericDefinitionDisplayString(typeParameters); + typeParameters.genericDefinitionDisplayString(); String notifierBuildType({ bool withGenericDefinition = false, @@ -389,7 +395,9 @@ extension ProviderNames on GeneratorProviderDeclaration { } } - String get elementName => switch (this) { + String get generatedElementName => '_\$${providerElement.name.public}Element'; + + String get internalElementName => switch (this) { ClassBasedProviderDeclaration() => switch (createdType) { SupportedCreatedType.future => r'$AsyncNotifierProviderElement', SupportedCreatedType.stream => r'$StreamNotifierProviderElement', @@ -417,16 +425,18 @@ extension ProviderNames on GeneratorProviderDeclaration { } } -String _genericDefinitionDisplayString(TypeParameterList? typeParameters) { - return typeParameters?.toSource() ?? ''; -} - -String _genericUsageDisplayString(TypeParameterList? typeParameterList) { - if (typeParameterList == null) { - return ''; +extension TypeX on TypeParameterList? { + String genericDefinitionDisplayString() { + return this?.toSource() ?? ''; } - return '<${typeParameterList.typeParameters.map((e) => e.name.lexeme).join(', ')}>'; + String genericUsageDisplayString() { + if (this == null) { + return ''; + } + + return '<${this!.typeParameters.map((e) => e.name.lexeme).join(', ')}>'; + } } extension ParameterDoc on AstNode { diff --git a/packages/riverpod_generator/lib/src/templates/element.dart b/packages/riverpod_generator/lib/src/templates/element.dart new file mode 100644 index 000000000..165f24602 --- /dev/null +++ b/packages/riverpod_generator/lib/src/templates/element.dart @@ -0,0 +1,95 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; + +import '../models.dart'; +import '../riverpod_generator.dart'; +import 'template.dart'; + +extension MutationX on Mutation { + String get elementFieldName => '_\$${name.lowerFirst}'; + String get generatedMutationInterfaceName => + '${(node.parent! as ClassDeclaration).name}\$${name.titled}'; + String get generatedMutationImplName => + '_\$${(node.parent! as ClassDeclaration).name}\$${name.titled}'; +} + +class ElementTemplate extends Template { + ElementTemplate(this.provider); + + final ClassBasedProviderDeclaration provider; + late final _generics = provider.generics(); + late final _genericsDefinition = provider.genericsDefinition(); + + @override + void run(StringBuffer buffer) { + if (provider.mutations.isEmpty) return; + + buffer.write(''' +class ${provider.generatedElementName}$_genericsDefinition extends ${provider.internalElementName}<${provider.name}$_generics, ${provider.valueTypeDisplayString}> { + ${provider.generatedElementName}(super.provider, super.pointer) { +'''); + + _constructorBody(buffer); + buffer.writeln('}'); + + _fields(buffer); + _overrideMount(buffer); + _overrideVisitChildren(buffer); + + buffer.writeln('}'); + } + + void _constructorBody(StringBuffer buffer) { + for (final mutation in provider.mutations) { + buffer.writeln( + ' ${mutation.elementFieldName}.result = Result.data(${mutation.generatedMutationImplName}(this));', + ); + } + } + + void _overrideVisitChildren(StringBuffer buffer) { + buffer.writeln(''' + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); +'''); + + for (final mutation in provider.mutations) { + buffer.writeln(' listenableVisitor(${mutation.elementFieldName});'); + } + + buffer.write(''' + } + '''); + } + + void _fields(StringBuffer buffer) { + for (final mutation in provider.mutations) { + buffer.writeln( + ' final ${mutation.elementFieldName} = ProxyElementValueListenable<${mutation.generatedMutationImplName}>();', + ); + } + } + + void _overrideMount(StringBuffer buffer) { + buffer.write(''' + @override + void mount() { + super.mount(); +'''); + for (final mutation in provider.mutations) { + buffer.writeln( + ' ${mutation.elementFieldName}.result!.stateOrNull!.reset();', + ); + } + buffer.writeln(''' + }'''); + } +} diff --git a/packages/riverpod_generator/lib/src/templates/mutation.dart b/packages/riverpod_generator/lib/src/templates/mutation.dart new file mode 100644 index 000000000..044ec019f --- /dev/null +++ b/packages/riverpod_generator/lib/src/templates/mutation.dart @@ -0,0 +1,100 @@ +import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; + +import '../riverpod_generator.dart'; +import '../type.dart'; +import 'element.dart'; +import 'parameters.dart'; +import 'template.dart'; + +class MutationTemplate extends Template { + MutationTemplate(this.mutation, this.provider); + + final Mutation mutation; + final ClassBasedProviderDeclaration provider; + + @override + void run(StringBuffer buffer) { + final parametersPassThrough = buildParamInvocationQuery({ + for (final parameter in mutation.node.parameters!.parameters) + parameter: parameter.name.toString(), + }); + + final mutationBase = switch (provider.createdType) { + SupportedCreatedType.future => r'$AsyncMutationBase', + SupportedCreatedType.stream => r'$AsyncMutationBase', + SupportedCreatedType.value => r'$SyncMutationBase', + }; + + buffer.writeln(''' +sealed class ${mutation.generatedMutationInterfaceName} extends MutationBase<${provider.valueTypeDisplayString}> { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [${provider.name}.${mutation.name}] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future<${provider.valueTypeDisplayString}> call${mutation.node.typeParameters.genericDefinitionDisplayString()}${mutation.node.parameters}; +} + +final class ${mutation.generatedMutationImplName} + extends $mutationBase<${provider.valueTypeDisplayString}, ${mutation.generatedMutationImplName}, ${provider.name}> + implements ${mutation.generatedMutationInterfaceName} { + ${mutation.generatedMutationImplName}(this.element, {super.state, super.key}); + + @override + final ${provider.generatedElementName} element; + + @override + ProxyElementValueListenable<${mutation.generatedMutationImplName}> get listenable => element.${mutation.elementFieldName}; + + @override + Future<${provider.valueTypeDisplayString}> call${mutation.node.typeParameters.genericDefinitionDisplayString()}${mutation.node.parameters} { + return mutateAsync( + ${_mutationInvocation()}, + (\$notifier) => \$notifier.${mutation.name}${mutation.node.typeParameters.genericUsageDisplayString()}($parametersPassThrough), + ); + } + + @override + ${mutation.generatedMutationImplName} copyWith(MutationState state, {Object? key}) => ${mutation.generatedMutationImplName}(element, state: state, key: key); +} +'''); + } + + String _mutationInvocation() { + final positional = mutation.node.parameters?.parameters + .where((e) => e.isPositional) + .map((e) => e.name!.lexeme) + .toList() ?? + const []; + Map? named = Map.fromEntries( + mutation.node.parameters?.parameters + .where((e) => e.isNamed) + .map((e) => MapEntry(e.name!.lexeme, e.name!.lexeme)) ?? + const [], + ); + + if (named.isEmpty) named = null; + + final typeParams = mutation.node.typeParameters?.typeParameters + .map((e) => e.name.lexeme) + .toList() ?? + const []; + if (typeParams.isEmpty) { + return 'Invocation.method(#${mutation.name}, $positional, ${named ?? ''})'; + } + + return 'Invocation.genericMethod(#${mutation.name}, $typeParams, $positional, ${named ?? ''})'; + } +} diff --git a/packages/riverpod_generator/lib/src/templates/provider.dart b/packages/riverpod_generator/lib/src/templates/provider.dart index 3c0b49614..967f0303f 100644 --- a/packages/riverpod_generator/lib/src/templates/provider.dart +++ b/packages/riverpod_generator/lib/src/templates/provider.dart @@ -3,6 +3,7 @@ import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; import '../models.dart'; import '../riverpod_generator.dart'; import '../type.dart'; +import 'element.dart'; import 'parameters.dart'; import 'template.dart'; @@ -162,9 +163,9 @@ ${provider.doc} final class $name$_genericsDefinition buffer.writeln(''' @\$internal @override - ${provider.elementName}<${provider.valueTypeDisplayString}> \$createElement( + ${provider.internalElementName}<${provider.valueTypeDisplayString}> \$createElement( \$ProviderPointer pointer - ) => ${provider.elementName}(this, pointer); + ) => ${provider.internalElementName}(this, pointer); @override ${provider.providerTypeName}$_generics \$copyWithCreate( @@ -179,7 +180,7 @@ ${provider.doc} final class $name$_genericsDefinition _writeFunctionalCreate(buffer); - case ClassBasedProviderDeclaration(): + case ClassBasedProviderDeclaration(:final mutations): final notifierType = '${provider.name}$_generics'; buffer.writeln(''' @@ -208,16 +209,52 @@ ${provider.doc} final class $name$_genericsDefinition runNotifierBuildOverride: build ); } +'''); + + _classCreateElement(mutations, buffer, notifierType); + + for (final mutation in mutations) { + buffer.writeln(''' + ProviderListenable<${mutation.generatedMutationInterfaceName}> get ${mutation.name} + => LazyProxyListenable<${mutation.generatedMutationInterfaceName}, ${provider.exposedTypeDisplayString}>( + this, + (element) { + element as ${provider.generatedElementName}$_generics; + + return element.${mutation.elementFieldName}; + }, + ); + '''); + } + } + + _writeEqual(buffer); + } + void _classCreateElement( + List mutations, + StringBuffer buffer, + String notifierType, + ) { + if (mutations.isEmpty) { + buffer.writeln(''' @\$internal @override - ${provider.elementName}<$notifierType, ${provider.valueTypeDisplayString}> \$createElement( + ${provider.internalElementName}<$notifierType, ${provider.valueTypeDisplayString}> \$createElement( \$ProviderPointer pointer - ) => ${provider.elementName}(this, pointer); + ) => ${provider.internalElementName}(this, pointer); '''); + + return; } - _writeEqual(buffer); + buffer.writeln(''' + @\$internal + @override + ${provider.generatedElementName}$_generics \$createElement( + \$ProviderPointer pointer + ) => ${provider.generatedElementName}(this, pointer); +'''); } void _writeOverrideWithValue(StringBuffer buffer) { diff --git a/packages/riverpod_generator/pubspec.yaml b/packages/riverpod_generator/pubspec.yaml index f406faa3d..b1298ea90 100644 --- a/packages/riverpod_generator/pubspec.yaml +++ b/packages/riverpod_generator/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: collection: ^1.15.0 crypto: ^3.0.2 meta: ^1.7.0 + mockito: ^5.4.4 path: ^1.8.0 riverpod_analyzer_utils: 1.0.0-dev.1 riverpod_annotation: 3.0.0-dev.3 diff --git a/packages/riverpod_generator/test/analysis_options.yaml b/packages/riverpod_generator/test/analysis_options.yaml new file mode 100644 index 000000000..ca4fba333 --- /dev/null +++ b/packages/riverpod_generator/test/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../analysis_options.yaml +analyzer: + errors: + # We have a tight constraint on Riverpod to use its internal APIs + invalid_use_of_internal_member: ignore diff --git a/packages/riverpod_generator/test/integration/mutation.dart b/packages/riverpod_generator/test/integration/mutation.dart new file mode 100644 index 000000000..08ccca44e --- /dev/null +++ b/packages/riverpod_generator/test/integration/mutation.dart @@ -0,0 +1,149 @@ +import 'package:riverpod/src/internals.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'mutation.g.dart'; + +@riverpod +class Simple extends _$Simple { + @override + int build() => 0; + + @mutation + Future increment([int inc = 1]) async => state + inc; + + @mutation + FutureOr incrementOr() => state + 1; + + @mutation + Future delegated(Future Function() fn) => fn(); +} + +@riverpod +class SimpleFamily extends _$SimpleFamily { + @override + int build(String arg) => 0; + + @mutation + Future increment([int inc = 1]) async => state + inc; + + @mutation + FutureOr incrementOr() => state + 1; +} + +@riverpod +class SimpleAsync extends _$SimpleAsync { + @override + Future build() async => 0; + + @mutation + Future increment([int inc = 1]) async => (await future) + inc; + + @mutation + Future delegated(Future Function() fn) async { + await future; + return fn(); + } +} + +@riverpod +class SimpleAsync2 extends _$SimpleAsync2 { + @override + Stream build(String arg) => Stream.value(0); + + @mutation + Future increment() async => (await future) + 1; +} + +@riverpod +class Generic extends _$Generic { + @override + Future build() async => 0; + + @mutation + Future increment() async => (await future) + 1; +} + +@riverpod +class GenericMut extends _$GenericMut { + @override + Future build() async => 0; + + @mutation + Future increment(T value) async => + (await future) + value.ceil(); +} + +@riverpod +class FailingCtor extends _$FailingCtor { + FailingCtor() { + throw StateError('err'); + } + + @override + int build() => 0; + + @mutation + Future increment([int inc = 1]) async => state + inc; +} + +// final mut = ref.watch(aProvider(arg).increment); +// mut(2); + +// class User { +// final String id; +// } + +// class UserSub extends User {} + +// class IList extends List {} + +// @Repository(retry: retry) +// class Users extends _$Users { +// @riverpod +// FutureOr byId(String id) => User(); + +// @riverpod +// Future> home() async => []; + +// @riverpod +// Future> search(String search) async => []; + +// @riverpod +// Future> search(String search) async => []; + +// @riverpod +// Future> search(String search) async => []; + +// @riverpod +// Stream> socketSearch(String search) async => []; + +// @mutation +// Future addUser(User user) async { +// return user; +// } +// } + +// abstract class Repository { +// Map> _all; + +// KeyT key(StateT state); + +// void build(Ref ref); +// } + +// abstract class _$Users extends Repository { +// // User has a .id, so we automatically pick it up as the key +// @override +// String key(User user) => user.id; + +// @override +// void build(ref) { +// observe(providerOrFamily, onAdd: (provider, key, value) { +// ref.mutate((state) => state.add(value)); +// }, onUpdate); +// } +// } + +// // ref.watch(usersProvider.byId('123')); +// // ref.watch(usersProvider.home); +// // ref.watch(usersProvider.search('john')); diff --git a/packages/riverpod_generator/test/integration/mutation.g.dart b/packages/riverpod_generator/test/integration/mutation.g.dart new file mode 100644 index 000000000..e229d3876 --- /dev/null +++ b/packages/riverpod_generator/test/integration/mutation.g.dart @@ -0,0 +1,1593 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mutation.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +@ProviderFor(Simple) +const simpleProvider = SimpleProvider._(); + +final class SimpleProvider extends $NotifierProvider { + const SimpleProvider._( + {super.runNotifierBuildOverride, Simple Function()? create}) + : _createCb = create, + super( + from: null, + argument: null, + retry: null, + name: r'simpleProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final Simple Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$simpleHash(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $ValueProvider(value), + ); + } + + @$internal + @override + Simple create() => _createCb?.call() ?? Simple(); + + @$internal + @override + SimpleProvider $copyWithCreate( + Simple Function() create, + ) { + return SimpleProvider._(create: create); + } + + @$internal + @override + SimpleProvider $copyWithBuild( + int Function( + Ref, + Simple, + ) build, + ) { + return SimpleProvider._(runNotifierBuildOverride: build); + } + + @$internal + @override + _$SimpleElement $createElement($ProviderPointer pointer) => + _$SimpleElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable( + this, + (element) { + element as _$SimpleElement; + + return element._$increment; + }, + ); + + ProviderListenable get incrementOr => + LazyProxyListenable( + this, + (element) { + element as _$SimpleElement; + + return element._$incrementOr; + }, + ); + + ProviderListenable get delegated => + LazyProxyListenable( + this, + (element) { + element as _$SimpleElement; + + return element._$delegated; + }, + ); +} + +String _$simpleHash() => r'c84cd9b6e3b09516b19316b0b21ea5ba5bc08a07'; + +abstract class _$Simple extends $Notifier { + int build(); + @$internal + @override + int runBuild() => build(); +} + +class _$SimpleElement extends $NotifierProviderElement { + _$SimpleElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$Simple$Increment(this)); + _$incrementOr.result = Result.data(_$Simple$IncrementOr(this)); + _$delegated.result = Result.data(_$Simple$Delegated(this)); + } + final _$increment = ProxyElementValueListenable<_$Simple$Increment>(); + final _$incrementOr = ProxyElementValueListenable<_$Simple$IncrementOr>(); + final _$delegated = ProxyElementValueListenable<_$Simple$Delegated>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + _$incrementOr.result!.stateOrNull!.reset(); + _$delegated.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + listenableVisitor(_$incrementOr); + listenableVisitor(_$delegated); + } +} + +sealed class Simple$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [Simple.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call([int inc = 1]); +} + +final class _$Simple$Increment + extends $SyncMutationBase + implements Simple$Increment { + _$Simple$Increment(this.element, {super.state, super.key}); + + @override + final _$SimpleElement element; + + @override + ProxyElementValueListenable<_$Simple$Increment> get listenable => + element._$increment; + + @override + Future call([int inc = 1]) { + return mutateAsync( + Invocation.method( + #increment, + [inc], + ), + ($notifier) => $notifier.increment( + inc, + ), + ); + } + + @override + _$Simple$Increment copyWith(MutationState state, {Object? key}) => + _$Simple$Increment(element, state: state, key: key); +} + +sealed class Simple$IncrementOr extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [Simple.incrementOr] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(); +} + +final class _$Simple$IncrementOr + extends $SyncMutationBase + implements Simple$IncrementOr { + _$Simple$IncrementOr(this.element, {super.state, super.key}); + + @override + final _$SimpleElement element; + + @override + ProxyElementValueListenable<_$Simple$IncrementOr> get listenable => + element._$incrementOr; + + @override + Future call() { + return mutateAsync( + Invocation.method( + #incrementOr, + [], + ), + ($notifier) => $notifier.incrementOr(), + ); + } + + @override + _$Simple$IncrementOr copyWith(MutationState state, {Object? key}) => + _$Simple$IncrementOr(element, state: state, key: key); +} + +sealed class Simple$Delegated extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [Simple.delegated] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(Future Function() fn); +} + +final class _$Simple$Delegated + extends $SyncMutationBase + implements Simple$Delegated { + _$Simple$Delegated(this.element, {super.state, super.key}); + + @override + final _$SimpleElement element; + + @override + ProxyElementValueListenable<_$Simple$Delegated> get listenable => + element._$delegated; + + @override + Future call(Future Function() fn) { + return mutateAsync( + Invocation.method( + #delegated, + [fn], + ), + ($notifier) => $notifier.delegated( + fn, + ), + ); + } + + @override + _$Simple$Delegated copyWith(MutationState state, {Object? key}) => + _$Simple$Delegated(element, state: state, key: key); +} + +@ProviderFor(SimpleFamily) +const simpleFamilyProvider = SimpleFamilyFamily._(); + +final class SimpleFamilyProvider extends $NotifierProvider { + const SimpleFamilyProvider._( + {required SimpleFamilyFamily super.from, + required String super.argument, + super.runNotifierBuildOverride, + SimpleFamily Function()? create}) + : _createCb = create, + super( + retry: null, + name: r'simpleFamilyProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final SimpleFamily Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$simpleFamilyHash(); + + @override + String toString() { + return r'simpleFamilyProvider' + '' + '($argument)'; + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $ValueProvider(value), + ); + } + + @$internal + @override + SimpleFamily create() => _createCb?.call() ?? SimpleFamily(); + + @$internal + @override + SimpleFamilyProvider $copyWithCreate( + SimpleFamily Function() create, + ) { + return SimpleFamilyProvider._( + argument: argument as String, + from: from! as SimpleFamilyFamily, + create: create); + } + + @$internal + @override + SimpleFamilyProvider $copyWithBuild( + int Function( + Ref, + SimpleFamily, + ) build, + ) { + return SimpleFamilyProvider._( + argument: argument as String, + from: from! as SimpleFamilyFamily, + runNotifierBuildOverride: build); + } + + @$internal + @override + _$SimpleFamilyElement $createElement($ProviderPointer pointer) => + _$SimpleFamilyElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable( + this, + (element) { + element as _$SimpleFamilyElement; + + return element._$increment; + }, + ); + + ProviderListenable get incrementOr => + LazyProxyListenable( + this, + (element) { + element as _$SimpleFamilyElement; + + return element._$incrementOr; + }, + ); + + @override + bool operator ==(Object other) { + return other is SimpleFamilyProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$simpleFamilyHash() => r'7f7a9985568e147b78fbcd6ed7691a6677f75aeb'; + +final class SimpleFamilyFamily extends Family { + const SimpleFamilyFamily._() + : super( + retry: null, + name: r'simpleFamilyProvider', + dependencies: null, + allTransitiveDependencies: null, + isAutoDispose: true, + ); + + SimpleFamilyProvider call( + String arg, + ) => + SimpleFamilyProvider._(argument: arg, from: this); + + @override + String debugGetCreateSourceHash() => _$simpleFamilyHash(); + + @override + String toString() => r'simpleFamilyProvider'; + + /// {@macro riverpod.override_with} + Override overrideWith( + SimpleFamily Function( + String args, + ) create, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as SimpleFamilyProvider; + + final argument = provider.argument as String; + + return provider + .$copyWithCreate(() => create(argument)) + .$createElement(pointer); + }, + ); + } + + /// {@macro riverpod.override_with_build} + Override overrideWithBuild( + int Function(Ref ref, SimpleFamily notifier, String argument) build, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as SimpleFamilyProvider; + + final argument = provider.argument as String; + + return provider + .$copyWithBuild((ref, notifier) => build(ref, notifier, argument)) + .$createElement(pointer); + }, + ); + } +} + +abstract class _$SimpleFamily extends $Notifier { + late final _$args = ref.$arg as String; + String get arg => _$args; + + int build( + String arg, + ); + @$internal + @override + int runBuild() => build( + _$args, + ); +} + +class _$SimpleFamilyElement + extends $NotifierProviderElement { + _$SimpleFamilyElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$SimpleFamily$Increment(this)); + _$incrementOr.result = Result.data(_$SimpleFamily$IncrementOr(this)); + } + final _$increment = ProxyElementValueListenable<_$SimpleFamily$Increment>(); + final _$incrementOr = + ProxyElementValueListenable<_$SimpleFamily$IncrementOr>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + _$incrementOr.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + listenableVisitor(_$incrementOr); + } +} + +sealed class SimpleFamily$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [SimpleFamily.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call([int inc = 1]); +} + +final class _$SimpleFamily$Increment + extends $SyncMutationBase + implements SimpleFamily$Increment { + _$SimpleFamily$Increment(this.element, {super.state, super.key}); + + @override + final _$SimpleFamilyElement element; + + @override + ProxyElementValueListenable<_$SimpleFamily$Increment> get listenable => + element._$increment; + + @override + Future call([int inc = 1]) { + return mutateAsync( + Invocation.method( + #increment, + [inc], + ), + ($notifier) => $notifier.increment( + inc, + ), + ); + } + + @override + _$SimpleFamily$Increment copyWith(MutationState state, {Object? key}) => + _$SimpleFamily$Increment(element, state: state, key: key); +} + +sealed class SimpleFamily$IncrementOr extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [SimpleFamily.incrementOr] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(); +} + +final class _$SimpleFamily$IncrementOr + extends $SyncMutationBase + implements SimpleFamily$IncrementOr { + _$SimpleFamily$IncrementOr(this.element, {super.state, super.key}); + + @override + final _$SimpleFamilyElement element; + + @override + ProxyElementValueListenable<_$SimpleFamily$IncrementOr> get listenable => + element._$incrementOr; + + @override + Future call() { + return mutateAsync( + Invocation.method( + #incrementOr, + [], + ), + ($notifier) => $notifier.incrementOr(), + ); + } + + @override + _$SimpleFamily$IncrementOr copyWith(MutationState state, + {Object? key}) => + _$SimpleFamily$IncrementOr(element, state: state, key: key); +} + +@ProviderFor(SimpleAsync) +const simpleAsyncProvider = SimpleAsyncProvider._(); + +final class SimpleAsyncProvider + extends $AsyncNotifierProvider { + const SimpleAsyncProvider._( + {super.runNotifierBuildOverride, SimpleAsync Function()? create}) + : _createCb = create, + super( + from: null, + argument: null, + retry: null, + name: r'simpleAsyncProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final SimpleAsync Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$simpleAsyncHash(); + + @$internal + @override + SimpleAsync create() => _createCb?.call() ?? SimpleAsync(); + + @$internal + @override + SimpleAsyncProvider $copyWithCreate( + SimpleAsync Function() create, + ) { + return SimpleAsyncProvider._(create: create); + } + + @$internal + @override + SimpleAsyncProvider $copyWithBuild( + FutureOr Function( + Ref, + SimpleAsync, + ) build, + ) { + return SimpleAsyncProvider._(runNotifierBuildOverride: build); + } + + @$internal + @override + _$SimpleAsyncElement $createElement($ProviderPointer pointer) => + _$SimpleAsyncElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable>( + this, + (element) { + element as _$SimpleAsyncElement; + + return element._$increment; + }, + ); + + ProviderListenable get delegated => + LazyProxyListenable>( + this, + (element) { + element as _$SimpleAsyncElement; + + return element._$delegated; + }, + ); +} + +String _$simpleAsyncHash() => r'ed00b8e5170e48855d0b3cddddabd316fef466cf'; + +abstract class _$SimpleAsync extends $AsyncNotifier { + FutureOr build(); + @$internal + @override + FutureOr runBuild() => build(); +} + +class _$SimpleAsyncElement + extends $AsyncNotifierProviderElement { + _$SimpleAsyncElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$SimpleAsync$Increment(this)); + _$delegated.result = Result.data(_$SimpleAsync$Delegated(this)); + } + final _$increment = ProxyElementValueListenable<_$SimpleAsync$Increment>(); + final _$delegated = ProxyElementValueListenable<_$SimpleAsync$Delegated>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + _$delegated.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + listenableVisitor(_$delegated); + } +} + +sealed class SimpleAsync$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [SimpleAsync.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call([int inc = 1]); +} + +final class _$SimpleAsync$Increment + extends $AsyncMutationBase + implements SimpleAsync$Increment { + _$SimpleAsync$Increment(this.element, {super.state, super.key}); + + @override + final _$SimpleAsyncElement element; + + @override + ProxyElementValueListenable<_$SimpleAsync$Increment> get listenable => + element._$increment; + + @override + Future call([int inc = 1]) { + return mutateAsync( + Invocation.method( + #increment, + [inc], + ), + ($notifier) => $notifier.increment( + inc, + ), + ); + } + + @override + _$SimpleAsync$Increment copyWith(MutationState state, {Object? key}) => + _$SimpleAsync$Increment(element, state: state, key: key); +} + +sealed class SimpleAsync$Delegated extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [SimpleAsync.delegated] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(Future Function() fn); +} + +final class _$SimpleAsync$Delegated + extends $AsyncMutationBase + implements SimpleAsync$Delegated { + _$SimpleAsync$Delegated(this.element, {super.state, super.key}); + + @override + final _$SimpleAsyncElement element; + + @override + ProxyElementValueListenable<_$SimpleAsync$Delegated> get listenable => + element._$delegated; + + @override + Future call(Future Function() fn) { + return mutateAsync( + Invocation.method( + #delegated, + [fn], + ), + ($notifier) => $notifier.delegated( + fn, + ), + ); + } + + @override + _$SimpleAsync$Delegated copyWith(MutationState state, {Object? key}) => + _$SimpleAsync$Delegated(element, state: state, key: key); +} + +@ProviderFor(SimpleAsync2) +const simpleAsync2Provider = SimpleAsync2Family._(); + +final class SimpleAsync2Provider + extends $StreamNotifierProvider { + const SimpleAsync2Provider._( + {required SimpleAsync2Family super.from, + required String super.argument, + super.runNotifierBuildOverride, + SimpleAsync2 Function()? create}) + : _createCb = create, + super( + retry: null, + name: r'simpleAsync2Provider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final SimpleAsync2 Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$simpleAsync2Hash(); + + @override + String toString() { + return r'simpleAsync2Provider' + '' + '($argument)'; + } + + @$internal + @override + SimpleAsync2 create() => _createCb?.call() ?? SimpleAsync2(); + + @$internal + @override + SimpleAsync2Provider $copyWithCreate( + SimpleAsync2 Function() create, + ) { + return SimpleAsync2Provider._( + argument: argument as String, + from: from! as SimpleAsync2Family, + create: create); + } + + @$internal + @override + SimpleAsync2Provider $copyWithBuild( + Stream Function( + Ref, + SimpleAsync2, + ) build, + ) { + return SimpleAsync2Provider._( + argument: argument as String, + from: from! as SimpleAsync2Family, + runNotifierBuildOverride: build); + } + + @$internal + @override + _$SimpleAsync2Element $createElement($ProviderPointer pointer) => + _$SimpleAsync2Element(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable>( + this, + (element) { + element as _$SimpleAsync2Element; + + return element._$increment; + }, + ); + + @override + bool operator ==(Object other) { + return other is SimpleAsync2Provider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$simpleAsync2Hash() => r'7b372f85f3e4f1c2a954402b82a9a7b68bbc1407'; + +final class SimpleAsync2Family extends Family { + const SimpleAsync2Family._() + : super( + retry: null, + name: r'simpleAsync2Provider', + dependencies: null, + allTransitiveDependencies: null, + isAutoDispose: true, + ); + + SimpleAsync2Provider call( + String arg, + ) => + SimpleAsync2Provider._(argument: arg, from: this); + + @override + String debugGetCreateSourceHash() => _$simpleAsync2Hash(); + + @override + String toString() => r'simpleAsync2Provider'; + + /// {@macro riverpod.override_with} + Override overrideWith( + SimpleAsync2 Function( + String args, + ) create, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as SimpleAsync2Provider; + + final argument = provider.argument as String; + + return provider + .$copyWithCreate(() => create(argument)) + .$createElement(pointer); + }, + ); + } + + /// {@macro riverpod.override_with_build} + Override overrideWithBuild( + Stream Function(Ref ref, SimpleAsync2 notifier, String argument) build, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as SimpleAsync2Provider; + + final argument = provider.argument as String; + + return provider + .$copyWithBuild((ref, notifier) => build(ref, notifier, argument)) + .$createElement(pointer); + }, + ); + } +} + +abstract class _$SimpleAsync2 extends $StreamNotifier { + late final _$args = ref.$arg as String; + String get arg => _$args; + + Stream build( + String arg, + ); + @$internal + @override + Stream runBuild() => build( + _$args, + ); +} + +class _$SimpleAsync2Element + extends $StreamNotifierProviderElement { + _$SimpleAsync2Element(super.provider, super.pointer) { + _$increment.result = Result.data(_$SimpleAsync2$Increment(this)); + } + final _$increment = ProxyElementValueListenable<_$SimpleAsync2$Increment>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + } +} + +sealed class SimpleAsync2$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [SimpleAsync2.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(); +} + +final class _$SimpleAsync2$Increment + extends $AsyncMutationBase + implements SimpleAsync2$Increment { + _$SimpleAsync2$Increment(this.element, {super.state, super.key}); + + @override + final _$SimpleAsync2Element element; + + @override + ProxyElementValueListenable<_$SimpleAsync2$Increment> get listenable => + element._$increment; + + @override + Future call() { + return mutateAsync( + Invocation.method( + #increment, + [], + ), + ($notifier) => $notifier.increment(), + ); + } + + @override + _$SimpleAsync2$Increment copyWith(MutationState state, {Object? key}) => + _$SimpleAsync2$Increment(element, state: state, key: key); +} + +@ProviderFor(Generic) +const genericProvider = GenericFamily._(); + +final class GenericProvider + extends $AsyncNotifierProvider, int> { + const GenericProvider._( + {required GenericFamily super.from, + super.runNotifierBuildOverride, + Generic Function()? create}) + : _createCb = create, + super( + argument: null, + retry: null, + name: r'genericProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final Generic Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$genericHash(); + + GenericProvider _copyWithCreate( + Generic Function() create, + ) { + return GenericProvider._( + from: from! as GenericFamily, create: create); + } + + GenericProvider _copyWithBuild( + FutureOr Function( + Ref, + Generic, + ) build, + ) { + return GenericProvider._( + from: from! as GenericFamily, runNotifierBuildOverride: build); + } + + @override + String toString() { + return r'genericProvider' + '<${T}>' + '()'; + } + + @$internal + @override + Generic create() => _createCb?.call() ?? Generic(); + + @$internal + @override + GenericProvider $copyWithCreate( + Generic Function() create, + ) { + return GenericProvider._(from: from! as GenericFamily, create: create); + } + + @$internal + @override + GenericProvider $copyWithBuild( + FutureOr Function( + Ref, + Generic, + ) build, + ) { + return GenericProvider._( + from: from! as GenericFamily, runNotifierBuildOverride: build); + } + + @$internal + @override + _$GenericElement $createElement($ProviderPointer pointer) => + _$GenericElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable>( + this, + (element) { + element as _$GenericElement; + + return element._$increment; + }, + ); + + @override + bool operator ==(Object other) { + return other is GenericProvider && + other.runtimeType == runtimeType && + other.argument == argument; + } + + @override + int get hashCode { + return Object.hash(runtimeType, argument); + } +} + +String _$genericHash() => r'4089b4d9b08bfff0256ad67cf35780a6409f7a87'; + +final class GenericFamily extends Family { + const GenericFamily._() + : super( + retry: null, + name: r'genericProvider', + dependencies: null, + allTransitiveDependencies: null, + isAutoDispose: true, + ); + + GenericProvider call() => GenericProvider._(from: this); + + @override + String debugGetCreateSourceHash() => _$genericHash(); + + @override + String toString() => r'genericProvider'; + + /// {@macro riverpod.override_with} + Override overrideWith( + Generic Function() create, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as GenericProvider; + + return provider._copyWithCreate(create).$createElement(pointer); + }, + ); + } + + /// {@macro riverpod.override_with_build} + Override overrideWithBuild( + FutureOr Function(Ref ref, Generic notifier) build, + ) { + return $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as GenericProvider; + + return provider._copyWithBuild(build).$createElement(pointer); + }, + ); + } +} + +abstract class _$Generic extends $AsyncNotifier { + FutureOr build(); + @$internal + @override + FutureOr runBuild() => build(); +} + +class _$GenericElement + extends $AsyncNotifierProviderElement, int> { + _$GenericElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$Generic$Increment(this)); + } + final _$increment = ProxyElementValueListenable<_$Generic$Increment>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + } +} + +sealed class Generic$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [Generic.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(); +} + +final class _$Generic$Increment + extends $AsyncMutationBase + implements Generic$Increment { + _$Generic$Increment(this.element, {super.state, super.key}); + + @override + final _$GenericElement element; + + @override + ProxyElementValueListenable<_$Generic$Increment> get listenable => + element._$increment; + + @override + Future call() { + return mutateAsync( + Invocation.method( + #increment, + [], + ), + ($notifier) => $notifier.increment(), + ); + } + + @override + _$Generic$Increment copyWith(MutationState state, {Object? key}) => + _$Generic$Increment(element, state: state, key: key); +} + +@ProviderFor(GenericMut) +const genericMutProvider = GenericMutProvider._(); + +final class GenericMutProvider extends $AsyncNotifierProvider { + const GenericMutProvider._( + {super.runNotifierBuildOverride, GenericMut Function()? create}) + : _createCb = create, + super( + from: null, + argument: null, + retry: null, + name: r'genericMutProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final GenericMut Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$genericMutHash(); + + @$internal + @override + GenericMut create() => _createCb?.call() ?? GenericMut(); + + @$internal + @override + GenericMutProvider $copyWithCreate( + GenericMut Function() create, + ) { + return GenericMutProvider._(create: create); + } + + @$internal + @override + GenericMutProvider $copyWithBuild( + FutureOr Function( + Ref, + GenericMut, + ) build, + ) { + return GenericMutProvider._(runNotifierBuildOverride: build); + } + + @$internal + @override + _$GenericMutElement $createElement($ProviderPointer pointer) => + _$GenericMutElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable>( + this, + (element) { + element as _$GenericMutElement; + + return element._$increment; + }, + ); +} + +String _$genericMutHash() => r'43acfc1b7cf59fb05f31ed4c2d5470422198feb0'; + +abstract class _$GenericMut extends $AsyncNotifier { + FutureOr build(); + @$internal + @override + FutureOr runBuild() => build(); +} + +class _$GenericMutElement + extends $AsyncNotifierProviderElement { + _$GenericMutElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$GenericMut$Increment(this)); + } + final _$increment = ProxyElementValueListenable<_$GenericMut$Increment>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + } +} + +sealed class GenericMut$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [GenericMut.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call(T value); +} + +final class _$GenericMut$Increment + extends $AsyncMutationBase + implements GenericMut$Increment { + _$GenericMut$Increment(this.element, {super.state, super.key}); + + @override + final _$GenericMutElement element; + + @override + ProxyElementValueListenable<_$GenericMut$Increment> get listenable => + element._$increment; + + @override + Future call(T value) { + return mutateAsync( + Invocation.genericMethod( + #increment, + [T], + [value], + ), + ($notifier) => $notifier.increment( + value, + ), + ); + } + + @override + _$GenericMut$Increment copyWith(MutationState state, {Object? key}) => + _$GenericMut$Increment(element, state: state, key: key); +} + +@ProviderFor(FailingCtor) +const failingCtorProvider = FailingCtorProvider._(); + +final class FailingCtorProvider extends $NotifierProvider { + const FailingCtorProvider._( + {super.runNotifierBuildOverride, FailingCtor Function()? create}) + : _createCb = create, + super( + from: null, + argument: null, + retry: null, + name: r'failingCtorProvider', + isAutoDispose: true, + dependencies: null, + allTransitiveDependencies: null, + ); + + final FailingCtor Function()? _createCb; + + @override + String debugGetCreateSourceHash() => _$failingCtorHash(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $ValueProvider(value), + ); + } + + @$internal + @override + FailingCtor create() => _createCb?.call() ?? FailingCtor(); + + @$internal + @override + FailingCtorProvider $copyWithCreate( + FailingCtor Function() create, + ) { + return FailingCtorProvider._(create: create); + } + + @$internal + @override + FailingCtorProvider $copyWithBuild( + int Function( + Ref, + FailingCtor, + ) build, + ) { + return FailingCtorProvider._(runNotifierBuildOverride: build); + } + + @$internal + @override + _$FailingCtorElement $createElement($ProviderPointer pointer) => + _$FailingCtorElement(this, pointer); + + ProviderListenable get increment => + LazyProxyListenable( + this, + (element) { + element as _$FailingCtorElement; + + return element._$increment; + }, + ); +} + +String _$failingCtorHash() => r'6cdef257a2d783fa5a606b411be0d23744766cdc'; + +abstract class _$FailingCtor extends $Notifier { + int build(); + @$internal + @override + int runBuild() => build(); +} + +class _$FailingCtorElement extends $NotifierProviderElement { + _$FailingCtorElement(super.provider, super.pointer) { + _$increment.result = Result.data(_$FailingCtor$Increment(this)); + } + final _$increment = ProxyElementValueListenable<_$FailingCtor$Increment>(); + @override + void mount() { + super.mount(); + _$increment.result!.stateOrNull!.reset(); + } + + @override + void visitChildren({ + required void Function(ProviderElement element) elementVisitor, + required void Function(ProxyElementValueListenable element) + listenableVisitor, + }) { + super.visitChildren( + elementVisitor: elementVisitor, + listenableVisitor: listenableVisitor, + ); + + listenableVisitor(_$increment); + } +} + +sealed class FailingCtor$Increment extends MutationBase { + /// Starts the mutation. + /// + /// This will first set the state to [PendingMutationState], then + /// will call [FailingCtor.increment] with the provided parameters. + /// + /// After the method completes, the mutation state will be updated to either + /// [SuccessMutationState] or [ErrorMutationState] based on if the method + /// threw or not. + /// + /// Lastly, if the method completes without throwing, the Notifier's state + /// will be updated with the new value. + /// + /// **Note**: + /// If the notifier threw in its constructor, the mutation won't start + /// and [call] will throw. + /// This should generally never happen though, as Notifiers are not supposed + /// to have logic in their constructors. + Future call([int inc = 1]); +} + +final class _$FailingCtor$Increment + extends $SyncMutationBase + implements FailingCtor$Increment { + _$FailingCtor$Increment(this.element, {super.state, super.key}); + + @override + final _$FailingCtorElement element; + + @override + ProxyElementValueListenable<_$FailingCtor$Increment> get listenable => + element._$increment; + + @override + Future call([int inc = 1]) { + return mutateAsync( + Invocation.method( + #increment, + [inc], + ), + ($notifier) => $notifier.increment( + inc, + ), + ); + } + + @override + _$FailingCtor$Increment copyWith(MutationState state, {Object? key}) => + _$FailingCtor$Increment(element, state: state, key: key); +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/packages/riverpod_generator/test/mock.dart b/packages/riverpod_generator/test/mock.dart new file mode 100644 index 000000000..04efbf137 --- /dev/null +++ b/packages/riverpod_generator/test/mock.dart @@ -0,0 +1,201 @@ +import 'package:mockito/mockito.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:test/test.dart'; + +class ListenerMock with Mock { + void call(Object? a, Object? b); +} + +typedef VerifyOnly = VerificationResult Function( + Mock mock, + T matchingInvocations, +); + +/// Syntax sugar for: +/// +/// ```dart +/// verify(mock()).called(1); +/// verifyNoMoreInteractions(mock); +/// ``` +VerifyOnly get verifyOnly { + final verification = verify; + + return (mock, invocation) { + final result = verification(invocation); + result.called(1); + verifyNoMoreInteractions(mock); + return result; + }; +} + +TypeMatcher> isMutationBase({ + TypeMatcher>? state, +}) { + var matcher = isA>(); + + if (state != null) { + matcher = matcher.having((e) => e.state, 'state', state); + } + + return matcher; +} + +TypeMatcher> isIdleMutationState() { + return isA>(); +} + +TypeMatcher> isPendingMutationState() { + return isA>(); +} + +TypeMatcher> isSuccessMutationState(T value) { + return isA>().having((e) => e.value, 'value', value); +} + +TypeMatcher> isErrorMutationState(Object error) { + return isA>().having((e) => e.error, 'error', error); +} + +enum InvocationKind { + method, + getter, + setter, +} + +TypeMatcher isInvocation({ + Object? memberName, + List? positionalArguments, + Map? namedArguments, + Object? typeArguments, + InvocationKind? kind, +}) { + var matcher = isA(); + + if (kind != null) { + switch (kind) { + case InvocationKind.method: + matcher = matcher.having((e) => e.isMethod, 'isMethod', true); + case InvocationKind.getter: + matcher = matcher.having((e) => e.isGetter, 'isGetter', true); + case InvocationKind.setter: + matcher = matcher.having((e) => e.isSetter, 'isSetter', true); + } + } + + if (typeArguments != null) { + matcher = matcher.having( + (e) => e.typeArguments, + 'typeArguments', + typeArguments, + ); + } + + if (memberName != null) { + matcher = matcher.having((e) => e.memberName, 'memberName', memberName); + } + + if (positionalArguments != null) { + matcher = matcher.having( + (e) => e.positionalArguments, + 'positionalArguments', + positionalArguments, + ); + } + + if (namedArguments != null) { + matcher = matcher.having( + (e) => e.namedArguments, + 'namedArguments', + namedArguments, + ); + } + + return matcher; +} + +class ObserverMock extends Mock implements ProviderObserver { + ObserverMock([this.label]); + + final String? label; + + @override + String toString() { + return label ?? super.toString(); + } + + @override + void didAddProvider( + ProviderObserverContext? context, + Object? value, + ); + + @override + void providerDidFail( + ProviderObserverContext? context, + Object? error, + StackTrace? stackTrace, + ); + + @override + void didUpdateProvider( + ProviderObserverContext? context, + Object? previousValue, + Object? newValue, + ); + + @override + void didDisposeProvider(ProviderObserverContext? context); + + @override + void mutationReset(ProviderObserverContext? context); + + @override + void mutationStart( + ProviderObserverContext? context, + MutationContext? mutation, + ); + + @override + void mutationError( + ProviderObserverContext? context, + MutationContext? mutation, + Object? error, + StackTrace? stackTrace, + ); + + @override + void mutationSuccess( + ProviderObserverContext? context, + MutationContext? mutation, + Object? result, + ); +} + +TypeMatcher isProviderObserverContext( + Object? provider, + Object? container, { + required Object? mutation, +}) { + var matcher = isA(); + + matcher = matcher.having((e) => e.provider, 'provider', provider); + matcher = matcher.having((e) => e.container, 'container', container); + matcher = matcher.having((e) => e.mutation, 'mutation', mutation); + + return matcher; +} + +TypeMatcher isMutationContext( + Object? invocation, { + Object? notifier, +}) { + var matcher = isA(); + + matcher = matcher.having((e) => e.invocation, 'invocation', invocation); + if (notifier != null) { + matcher = matcher.having((e) => e.notifier, 'notifier', notifier); + } + + return matcher; +} diff --git a/packages/riverpod_generator/test/mutation_test.dart b/packages/riverpod_generator/test/mutation_test.dart new file mode 100644 index 000000000..27c19696f --- /dev/null +++ b/packages/riverpod_generator/test/mutation_test.dart @@ -0,0 +1,513 @@ +import 'dart:async'; + +import 'package:mockito/mockito.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:test/test.dart'; + +import 'integration/mutation.dart'; +import 'mock.dart'; + +void main() { + test('Can listen a mutation', () async { + final container = ProviderContainer.test(); + final listener = ListenerMock(); + + final sub = container.listen( + simpleProvider.delegated, + listener.call, + fireImmediately: true, + ); + + verifyOnly( + listener, + listener(any, isMutationBase(state: isIdleMutationState())), + ); + + final future = sub.read().call(() async => 1); + + verifyOnly( + listener, + listener(any, isMutationBase(state: isPendingMutationState())), + ); + + expect(await future, 1); + + verifyOnly( + listener, + listener(any, isMutationBase(state: isSuccessMutationState(1))), + ); + + final future2 = sub.read().call(() => throw StateError('42')); + + await expectLater(future2, throwsA(isStateError)); + verifyInOrder([ + listener(any, isMutationBase(state: isPendingMutationState())), + listener( + any, + isMutationBase(state: isErrorMutationState(isStateError)), + ), + ]); + verifyNoMoreInteractions(listener); + }); + + test('Can listen a mutation with family', () async { + final container = ProviderContainer.test(); + final listener = ListenerMock(); + + final sub = + container.listen(simpleFamilyProvider('key').increment, listener.call); + + expect( + sub.read(), + isMutationBase(state: isIdleMutationState()), + ); + + final future = sub.read().call(2); + + expect( + sub.read(), + isMutationBase(state: isPendingMutationState()), + ); + + expect(await future, 2); + + expect( + sub.read(), + isMutationBase(state: isSuccessMutationState(2)), + ); + }); + + test('Supports generic mutations', () async { + final container = ProviderContainer.test(); + final listener = ListenerMock(); + + final sub = container.listen(genericMutProvider.increment, listener.call); + + expect( + sub.read(), + isMutationBase(state: isIdleMutationState()), + ); + + final future = sub.read().call(2.5); + + expect(await future, 3); + + expect( + sub.read(), + isMutationBase(state: isSuccessMutationState(3)), + ); + }); + + group('auto reset', () { + test('Automatically resets the state when all listeners are removed', + () async { + final container = ProviderContainer.test(); + + // The mutation should reset even if the provider is kept alive + container.listen(simpleProvider, (a, b) {}); + + final sub = container.listen(simpleProvider.increment, (a, b) {}); + final sub2 = container.listen(simpleProvider.increment, (a, b) {}); + + await sub.read().call(2); + + sub.close(); + await null; + + expect( + container.read(simpleProvider.increment), + isMutationBase(state: isSuccessMutationState(2)), + ); + + sub2.close(); + await null; + + expect( + container.read(simpleProvider.increment), + isMutationBase(state: isIdleMutationState()), + ); + }); + + test('is cancelled if a listener is added during the delay', () async { + final container = ProviderContainer.test(); + + final sub = container.listen(simpleProvider.increment, (a, b) {}); + + await sub.read().call(2); + sub.close(); + + container.listen(simpleProvider.increment, (a, b) {}); + await null; + + expect( + container.read(simpleProvider.increment), + isMutationBase(state: isSuccessMutationState(2)), + ); + }); + }); + + test('Maintains progress even if a provider is when the provider is reset', + () async { + final container = ProviderContainer.test(); + + final sub = container.listen(simpleProvider.increment, (a, b) {}); + + await sub.read().call(2); + container.invalidate(simpleProvider); + + expect( + sub.read(), + isMutationBase(state: isSuccessMutationState(2)), + ); + }); + + test('Supports getting called again while pending', () async { + final container = ProviderContainer.test(); + final sub = container.listen(simpleAsyncProvider.delegated, (a, b) {}); + + final completer = Completer(); + final completer2 = Completer(); + final completer3 = Completer(); + + final future = sub.read().call(() => completer.future); + final future2 = sub.read().call(() => completer2.future); + final future3 = sub.read().call(() => completer3.future); + + completer.complete(42); + + expect(await future, 42); + expect( + sub.read(), + isMutationBase(state: isPendingMutationState()), + ); + expect(container.read(simpleAsyncProvider), const AsyncData(42)); + + completer2.completeError(21); + await expectLater(future2, throwsA(21)); + expect( + sub.read(), + isMutationBase(state: isPendingMutationState()), + ); + expect(container.read(simpleAsyncProvider), const AsyncData(42)); + + completer3.complete(21); + expect(await future3, 21); + expect( + sub.read(), + isMutationBase(state: isSuccessMutationState(21)), + ); + expect(container.read(simpleAsyncProvider), const AsyncData(21)); + }); + + test('Listening to a mutation keeps the provider alive', () async { + final container = ProviderContainer.test(); + + final sub = container.listen(simpleProvider.increment, (a, b) {}); + + expect(container.read(simpleProvider), 0); + + await container.pump(); + expect(container.exists(simpleProvider), true); + + sub.close(); + + await container.pump(); + expect(container.exists(simpleProvider), false); + }); + + test('Listening a mutation lazily initializes the provider', () async { + final container = ProviderContainer.test(); + + final sub = container.listen(simpleProvider.increment, (a, b) {}); + + final element = container.readProviderElement(simpleProvider); + + expect(element.stateResult, null); + + await sub.read().call(2); + + expect(container.read(simpleProvider), 2); + }); + + test('If notifier constructor throws, the mutation immediately throws', + () async { + final observer = ObserverMock(); + final container = ProviderContainer.test(observers: [observer]); + + final sub = container.listen(failingCtorProvider.increment, (a, b) {}); + + expect(sub.read(), isMutationBase(state: isIdleMutationState())); + + expect(() => sub.read().call(2), throwsStateError); + + expect( + sub.read(), + isMutationBase(state: isIdleMutationState()), + ); + verifyNever(observer.mutationError(any, any, any, any)); + }); + + group('reset', () { + test('Supports calling reset while pending', () async { + final container = ProviderContainer.test(); + final sub = container.listen(simpleProvider.delegated, (a, b) {}); + + final completer = Completer(); + final future = sub.read().call(() => completer.future); + + sub.read().reset(); + + completer.complete(42); + + expect(await future, 42); + expect( + sub.read(), + isMutationBase(state: isIdleMutationState()), + ); + }); + + test('sets the state back to idle', () async { + final container = ProviderContainer.test(); + final listener = ListenerMock(); + + final sub = container.listen(simpleProvider.increment, listener.call); + + await sub.read().call(2); + + sub.read().reset(); + + expect( + sub.read(), + isMutationBase(state: isIdleMutationState()), + ); + }); + }); + + group('Integration with ProviderObserver', () { + test('handles generic methods', () async { + final observer = ObserverMock(); + final container = ProviderContainer.test( + observers: [observer], + ); + + container.listen(genericMutProvider, (a, b) {}); + container.listen(genericMutProvider.increment, (a, b) {}); + await container.read(genericMutProvider.future); + + clearInteractions(observer); + + await container.read(genericMutProvider.increment).call(42); + + verify( + observer.didUpdateProvider( + argThat( + isProviderObserverContext( + genericMutProvider, + container, + mutation: isMutationContext( + isInvocation( + memberName: #increment, + positionalArguments: [42.0], + kind: InvocationKind.method, + typeArguments: [double], + ), + ), + ), + ), + const AsyncData(0), + const AsyncData(42), + ), + ); + }); + + test('sends current mutation to didUpdateProvider', () async { + final observer = ObserverMock(); + final container = ProviderContainer.test( + observers: [observer], + ); + + final sub = container.listen(simpleProvider.notifier, (a, b) {}); + container.listen(simpleProvider.delegated, (a, b) {}); + + clearInteractions(observer); + + Future fn() async { + sub.read().state = 1; + return 42; + } + + await container.read(simpleProvider.delegated).call(fn); + + verifyInOrder([ + observer.didUpdateProvider( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + ), + 0, + 1, + ), + observer.didUpdateProvider( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + ), + 1, + 42, + ), + ]); + }); + + test('handles mutationStart/Pending/Success/Error/Reset', () async { + final observer = ObserverMock(); + final container = ProviderContainer.test( + observers: [observer], + ); + + container.listen(simpleProvider, (a, b) {}); + clearInteractions(observer); + + final sub = container.listen(simpleProvider.delegated, (a, b) {}); + verifyNoMoreInteractions(observer); + + Future fn() async => 42; + final stack = StackTrace.current; + final err = StateError('foo'); + Future fn2() async => Error.throwWithStackTrace(err, stack); + + final future = sub.read().call(fn); + verifyOnly( + observer, + observer.mutationStart( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isNotNull, + ), + ), + argThat( + isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + ), + ); + + await future; + + verify( + observer.mutationSuccess( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isNotNull, + ), + ), + argThat( + isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + 42, + ), + ); + + final future2 = sub.read().call(fn2); + + verify( + observer.mutationStart( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isNotNull, + ), + ), + argThat( + isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn2], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + ), + ); + + await expectLater(future2, throwsA(isStateError)); + + verify( + observer.mutationError( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isNotNull, + ), + ), + argThat( + isMutationContext( + isInvocation( + memberName: #delegated, + positionalArguments: [fn2], + kind: InvocationKind.method, + typeArguments: isEmpty, + ), + ), + ), + err, + stack, + ), + ); + + sub.read().reset(); + + verify( + observer.mutationReset( + argThat( + isProviderObserverContext( + simpleProvider, + container, + mutation: isNull, + ), + ), + ), + ); + }); + }); +} diff --git a/website/docs/concepts/provider_observer_logger.dart b/website/docs/concepts/provider_observer_logger.dart index 8ab5ecbae..8aa41b77c 100644 --- a/website/docs/concepts/provider_observer_logger.dart +++ b/website/docs/concepts/provider_observer_logger.dart @@ -11,14 +11,13 @@ import 'package:flutter_riverpod/legacy.dart'; class Logger extends ProviderObserver { @override void didUpdateProvider( - ProviderBase provider, + ProviderObserverContext context, Object? previousValue, Object? newValue, - ProviderContainer container, ) { print(''' { - "provider": "${provider.name ?? provider.runtimeType}", + "provider": "${context.provider.name ?? context.provider.runtimeType}", "newValue": "$newValue" }'''); } diff --git a/website/docs/essentials/provider_observer/provider_observer.dart b/website/docs/essentials/provider_observer/provider_observer.dart index dd75afb01..27c668b2c 100644 --- a/website/docs/essentials/provider_observer/provider_observer.dart +++ b/website/docs/essentials/provider_observer/provider_observer.dart @@ -6,38 +6,34 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; class MyObserver extends ProviderObserver { @override void didAddProvider( - ProviderBase provider, + ProviderObserverContext context, Object? value, - ProviderContainer container, ) { - print('Provider $provider was initialized with $value'); + print('Provider ${context.provider} was initialized with $value'); } @override - void didDisposeProvider( - ProviderBase provider, - ProviderContainer container, - ) { - print('Provider $provider was disposed'); + void didDisposeProvider(ProviderObserverContext context) { + print('Provider ${context.provider} was disposed'); } @override void didUpdateProvider( - ProviderBase provider, + ProviderObserverContext context, Object? previousValue, Object? newValue, - ProviderContainer container, ) { - print('Provider $provider updated from $previousValue to $newValue'); + print( + 'Provider ${context.provider} updated from $previousValue to $newValue', + ); } @override void providerDidFail( - ProviderBase provider, + ProviderObserverContext context, Object error, StackTrace stackTrace, - ProviderContainer container, ) { - print('Provider $provider threw $error at $stackTrace'); + print('Provider ${context.provider} threw $error at $stackTrace'); } }