Skip to main content

bound_mutation 2.3.0 copy "bound_mutation: ^2.3.0" to clipboard
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 to run
  • BoundAction<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.run and Mutation.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().

3
likes
160
points
1.6k
downloads

Documentation

API reference

Publisher

verified publisherfirebon.de

Weekly Downloads

A small wrapper class around Riverpod Mutations to bind a function to a mutation.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

meta, riverpod

More

Packages that depend on bound_mutation