Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ask mutations to manually call state= #3983

Merged
merged 3 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 84 additions & 82 deletions packages/riverpod/lib/src/mutation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,22 @@ const mutationZoneKey = #_mutation;
/// 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<Todo>`, so the mutation
/// must return a `Future<List<Todo>>`.
///
/// ```dart
/// @riverpod
/// class TodoListNotifier extends $ExampleNotifier {
/// /* ... */
///
/// @mutation
/// Future<List<Todo>> addTodo(Todo todo) async {
/// Future<Todo> addTodo(String task) async {
/// final todo = Todo(task);
/// /* 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];
/// state = AsyncData([...await future, todo]);
///
/// // We return the new todo, so that the UI can display it.
/// return todo;
/// }
/// }
/// ```
Expand Down Expand Up @@ -115,7 +113,7 @@ const mutationZoneKey = #_mutation;
///
/// return ElevatedButton(
/// // Pressing the button will call `TodoListNotifier.addTodo`
/// onPressed: () => addTodo(Todo('Buy milk')),
/// onPressed: () => addTodo('Buy milk'),
/// );
/// ```
///
Expand All @@ -142,7 +140,7 @@ const mutationZoneKey = #_mutation;
/// 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');
/// print('The mutation has succeeded, and $value was returned');
/// }
/// ```
///
Expand Down Expand Up @@ -229,7 +227,7 @@ const mutationZoneKey = #_mutation;
/// 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
/// : () => addTodo('Buy milk'), // Otherwise enable the button
/// );
/// }
/// }
Expand Down Expand Up @@ -355,54 +353,88 @@ abstract class MutationBase<ResultT> {

@internal
abstract class $SyncMutationBase<
StateT,
MutationT extends $SyncMutationBase<StateT, MutationT, ClassT>,
ClassT extends NotifierBase<StateT>>
extends _MutationBase<StateT, StateT, MutationT, ClassT> {
ResultT,
MutationT extends $SyncMutationBase<ResultT, MutationT, ClassT>,
ClassT extends NotifierBase<Object?>>
extends _MutationBase<ResultT, MutationT, ClassT> {
$SyncMutationBase({super.state, super.key});

@override
void setData(StateT value) {
element.setStateResult($Result.data(value));
@protected
ResultT mutate(
Invocation invocation,
ResultT Function(ClassT clazz) cb,
) {
return _run(invocation, (_, notifier) => cb(notifier));
}
}

@internal
abstract class $AsyncMutationBase<
StateT,
MutationT extends $AsyncMutationBase<StateT, MutationT, ClassT>,
ClassT extends NotifierBase<AsyncValue<StateT>>>
extends _MutationBase<StateT, AsyncValue<StateT>, MutationT, ClassT> {
ResultT,
MutationT extends $AsyncMutationBase<ResultT, MutationT, ClassT>,
ClassT extends NotifierBase<Object?>>
extends _MutationBase<ResultT, MutationT, ClassT> {
$AsyncMutationBase({super.state, super.key});

@override
void setData(StateT value) {
element.setStateResult($Result.data(AsyncData(value)));
@protected
Future<ResultT> mutate(
Invocation invocation,
FutureOr<ResultT> Function(ClassT clazz) cb,
) {
return _run(
invocation,
(mutationContext, notifier) async {
// ! is safe because of the flush() above
final key = Object();
try {
_setState(
mutationContext,
copyWith(PendingMutationState<ResultT>._(), key: key),
);

final result = await cb(notifier);
if (key == _currentKey) {
_setState(
mutationContext,
copyWith(SuccessMutationState<ResultT>._(result)),
);
}

return result;
} catch (err, stack) {
if (key == _currentKey) {
_setState(
mutationContext,
copyWith(ErrorMutationState<ResultT>._(err, stack)),
);
}

rethrow;
}
},
);
}
}

abstract class _MutationBase<
ValueT,
StateT,
MutationT extends _MutationBase<ValueT, StateT, MutationT, ClassT>,
ClassT extends NotifierBase<StateT>> implements MutationBase<ValueT> {
_MutationBase({MutationState<ValueT>? state, this.key})
: state = state ?? IdleMutationState<ValueT>._() {
ResultT,
MutationT extends _MutationBase<ResultT, MutationT, ClassT>,
ClassT extends NotifierBase<Object?>> implements MutationBase<ResultT> {
_MutationBase({MutationState<ResultT>? state, this.key})
: state = state ?? IdleMutationState<ResultT>._() {
listenable.onCancel = _scheduleAutoReset;
}

@override
final MutationState<ValueT> state;
final MutationState<ResultT> state;
final Object? key;

$ClassProviderElement<ClassT, StateT, ValueT, Object?> get element;
$ClassProviderElement<ClassT, Object?, Object?, Object?> get element;
$ElementLense<MutationT> get listenable;

Object? get _currentKey => listenable.result?.stateOrNull?.key;

MutationT copyWith(MutationState<ValueT> state, {Object? key});

void setData(ValueT value);
MutationT copyWith(MutationState<ResultT> state, {Object? key});

void _scheduleAutoReset() {
Future.microtask(() {
Expand All @@ -414,15 +446,29 @@ abstract class _MutationBase<

@override
void reset() {
if (state is IdleMutationState<ValueT>) return;
if (state is IdleMutationState<ResultT>) return;

listenable.result = ResultData(copyWith(IdleMutationState<ValueT>._()));
listenable.result = ResultData(copyWith(IdleMutationState<ResultT>._()));

final context = ProviderObserverContext(element.origin, element.container);

_notifyObserver((obs) => obs.mutationReset(context));
}

T _run<T>(
Invocation invocation,
T Function(MutationContext mutationContext, ClassT notifier) cb,
) {
element.flush();
final notifier = element.classListenable.value;
final mutationContext = MutationContext(invocation, notifier);

return runZoned(
zoneValues: {mutationZoneKey: mutationContext},
() => cb(mutationContext, notifier),
);
}

void _notifyObserver(void Function(ProviderObserver obs) cb) {
for (final observer in element.container.observers) {
runUnaryGuarded(cb, observer);
Expand Down Expand Up @@ -463,50 +509,6 @@ abstract class _MutationBase<
}
}

@protected
Future<ValueT> mutateAsync(
Invocation invocation,
FutureOr<ValueT> 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<ValueT>._(), key: key),
);

final result = await cb(notifier);
if (key == _currentKey) {
_setState(
mutationContext,
copyWith(SuccessMutationState<ValueT>._(result)),
);
}
setData(result);

return result;
} catch (err, stack) {
if (key == _currentKey) {
_setState(
mutationContext,
copyWith(ErrorMutationState<ValueT>._(err, stack)),
);
}

rethrow;
}
},
);
}

@override
String toString() => '$runtimeType#${shortHash(this)}($state)';
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export 'src/errors.dart' hide ErrorReporter;
export 'src/errors.dart';
export 'src/nodes.dart'
hide
parseFirstProviderFor,
Expand Down
8 changes: 3 additions & 5 deletions packages/riverpod_analyzer_utils/lib/src/errors.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:meta/meta.dart';

@internal
typedef ErrorReporter = void Function(RiverpodAnalysisError);
typedef RiverpodErrorReporter = void Function(RiverpodAnalysisError);

ErrorReporter errorReporter = (error) {
RiverpodErrorReporter errorReporter = (error) {
throw UnsupportedError(
'RiverpodAnalysisError found but no errorReporter specified: $error',
);
Expand All @@ -19,9 +17,9 @@ enum RiverpodAnalysisErrorCode {
providerDependencyListParseError,
providerOrFamilyExpressionParseError,
invalidRetryArgument,
mutationReturnTypeMismatch,
mutationIsStatic,
mutationIsAbstract,
unsupportedMutationReturnType,
}

class RiverpodAnalysisError {
Expand Down
1 change: 0 additions & 1 deletion packages/riverpod_analyzer_utils/lib/src/nodes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ 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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension FunctionalProviderDeclarationX on FunctionDeclaration {
annotation: riverpod,
createdTypeNode: createdTypeNode,
exposedTypeNode: exposedTypeNode,
valueTypeNode: _getValueType(createdTypeNode, element.library),
valueTypeNode: _getValueType(createdTypeNode),
);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ extension ClassBasedProviderDeclarationX on ClassDeclaration {
return e.annotationOfType(riverpodPersistType, exact: false) != null;
});

final valueTypeNode = _getValueType(createdTypeNode, element.library);
final valueTypeNode = _getValueType(createdTypeNode);
final classBasedProviderDeclaration = ClassBasedProviderDeclaration._(
name: name,
node: this,
Expand Down Expand Up @@ -180,40 +180,37 @@ extension MutationMethodDeclarationX on MethodDeclaration {
?.returnType;
if (expectedReturnType == null) return null;

final expectedValueType = _getValueType(
expectedReturnType,
element.library,
);
final expectedValueType = _getValueType(expectedReturnType);
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 returnType = this.returnType;
final createdType = SupportedCreatedType.from(returnType);
String? valueDisplayString;
switch (createdType) {
case SupportedCreatedType.future:
valueDisplayString = (returnType! as NamedType)
.typeArguments
?.arguments
.firstOrNull
?.toSource();
case SupportedCreatedType.stream:
errorReporter(
RiverpodAnalysisError(
'Mutations returning Streams are not supported',
code: RiverpodAnalysisErrorCode.unsupportedMutationReturnType,
targetNode: this,
targetElement: element,
),
);
case SupportedCreatedType.value:
valueDisplayString = returnType?.toSource();
}

final mutation = Mutation._(
node: this,
element: mutationElement,
createdType: createdType,
valueDisplayType: valueDisplayString ?? '',
);

return mutation;
Expand All @@ -225,9 +222,13 @@ final class Mutation {
Mutation._({
required this.node,
required this.element,
required this.valueDisplayType,
required this.createdType,
});

String get name => node.name.lexeme;
final String valueDisplayType;
final SupportedCreatedType createdType;
final MethodDeclaration node;
final MutationElement element;
}
Expand Down
Loading
Loading