bound_mutation 2.3.0
bound_mutation: ^2.3.0 copied to clipboard
A small wrapper class around Riverpod Mutations to bind a function to a mutation.
bound_mutation #
A wrapper around riverpod's Mutation that binds an input parameter to the mutation callback. Instead of capturing input at construction time, you pass it when calling run.
Features #
BoundMutation<ResultT, InputR>— mutation with an input parameter passed torunBoundAction<ResultT>— mutation without input- Keys —
deleteUser(id)gives every id its own idle/pending/success/error state - Full
ProviderListenable<MutationState<ResultT>>integration — listen to idle/pending/success/error states - Thin wrapper: delegates directly to
Mutation.runandMutation.reset
Usage #
A complete, runnable example lives in example/main.dart
(dart run example/main.dart) — it covers run, error states, reset,
cascade and keys without needing Flutter.
import 'package:bound_mutation/bound_mutation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Define a bound mutation: ResultT = String, InputR = int
final createUser = BoundMutation<String, int>(
(transaction, userId) async {
// transaction gives access to providers
final repo = transaction.get(userRepositoryProvider);
return repo.fetchUserName(userId);
},
);
// Inside a provider or widget with a Ref:
Future<void> loadUser(WidgetRef ref) async {
// Watch mutation state
final state = ref.watch(createUser);
if (state.isIdle) {
final name = await createUser.run(ref, 42);
print('User: $name');
} else if (state.isPending) {
print('Loading...');
} else if (state.isSuccess) {
print('Success: ${(state as MutationSuccess<String>).value}');
} else if (state.hasError) {
final err = state as MutationError<String>;
print('Error: ${err.error}');
}
}
// Reset mutation state (back to idle):
createUser.reset(ref);
With ProviderContainer (e.g. in tests) #
final container = ProviderContainer();
final result = await createUser.run(container, 42);
createUser.reset(container);
container.dispose();
BoundAction<ResultT> — mutation without input #
final refreshFeed = BoundAction<void>((transaction) async {
final repo = transaction.get(feedRepositoryProvider);
await repo.refresh();
});
await refreshFeed.run(ref);
cascade — reusing a mutation inside another one #
Call cascade from within another mutation's callback to reuse its logic in the
same transaction. The cascaded mutation is not run, so its state stays as it is —
only the outer mutation reports pending/success/error:
final refreshEverything = BoundAction<void>((transaction) async {
await refreshFeed.cascade(transaction);
await createUser.cascade(transaction, 42);
});
Keys — one state per value #
By default every run writes to the same state, so a single deleteUser mutation
cannot tell which user is currently being deleted. Calling the mutation with a
key returns an instance with a state of its own, exactly like riverpod's
Mutation.call:
final deleteUser = BoundMutation<void, int>((transaction, userId) async {
await transaction.get(userRepositoryProvider).delete(userId);
});
// In a list, each row watches and runs its own key:
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(deleteUser(user.id));
return ElevatedButton(
onPressed: state.isPending
? null
: () => deleteUser(user.id).run(ref, user.id),
child: state.isPending ? const CircularProgressIndicator() : const Text('Delete'),
);
}
The key is not derived from the input — watching and running have to pass the
same key explicitly. Keys are matched with ==, so custom key objects should
override ==/hashCode; use a record for a composite key
(deleteUser((user.id, listId))).
Keyed instances are independent of the unkeyed one and of each other: run,
reset and watching only ever affect the state of the key they were used with.
Two instances created from the same key compare equal, so calling deleteUser(id)
inline in a build method is safe.
BoundAction works the same way:
final state = ref.watch(syncDevice(device.id));
await syncDevice(device.id).run(ref);
API #
| Method | Description |
|---|---|
BoundMutation(cb, {label}) |
Creates a mutation with a callback (transaction, input) -> Future<ResultT> |
BoundMutation.run(target, input) |
Executes the mutation, returning Future<ResultT> |
BoundMutation.cascade(tsx, input) |
Runs the callback inside an existing transaction, leaving this mutation's state untouched |
BoundAction(cb, {label}) |
Creates a mutation without input, callback (transaction) -> Future<ResultT> |
BoundAction.run(target) |
Executes the mutation, returning Future<ResultT> |
BoundAction.cascade(tsx) |
Runs the callback inside an existing transaction, leaving this mutation's state untouched |
bound(key) (call) |
Returns an instance with the same callback whose state is scoped to key |
key |
The key this instance is scoped to, or null if it is unkeyed |
reset(target) |
Resets the mutation state back to MutationIdle (of this key only) |
source |
Returns the underlying Mutation<ResultT> |
== / hashCode |
Delegates to the internal Mutation |
BoundMutation and BoundAction implement ProviderListenable<MutationState<ResultT>>, so they can be watched via ref.watch() or container.listen().