riverpod_mutation_utils 0.3.2
riverpod_mutation_utils: ^0.3.2 copied to clipboard
Runtime helpers, annotations, and mixins for Riverpod experimental mutations.
riverpod_mutation_utils #
Runtime helpers, annotations, and mixins for Riverpod experimental mutations.
For local development in this package, run dart run build_runner build --delete-conflicting-outputs
inside packages/riverpod_mutation_utils
before dart analyze or dart test. Example .g.dart files are generated in CI and are not committed.
This package extracts the non-UI mutation layer so multiple apps can share:
- reset-on-dispose mutation handling
- in-flight submit coalescing
- sync and async form mixins
- mutation-only action mixins
Scope #
This package is intentionally small. It does not include:
- dialogs or pages
- toasts or error banners
- navigation helpers
- form widgets
Keep those concerns inside each app.
Install #
Runtime package:
dependencies:
riverpod_mutation_utils: ^0.3.2
If you use riverpod_annotation, also add:
dependencies:
riverpod_annotation: ^4.0.2
dev_dependencies:
build_runner: ^2.7.1
riverpod_generator: ^4.0.2
If you want generated mutation wiring, also add:
dev_dependencies:
riverpod_mutation_utils_generator: ^0.3.2
Quick Start #
Pick one integration level:
MutationRunnerif you are not usingriverpod_annotationStateFormMixin/AsyncStateFormMixinif you want handwritten mutation wiring@generateMutationif you want family-safe mutation wiring generated for you
The common shape is:
- Define a stable
Mutation<Result>base. - Run the mutation with
submit(...)orsubmitAction(...). - Watch the mutation accessor from the UI.
Example UI usage:
final mutation = ref.watch(itemUpdateFormMutation('item-1'));
if (mutation is MutationPending<String>) {
return const CircularProgressIndicator();
}
afterSuccess runs after the transaction has closed. Use it for post-success
side effects that can safely use ref when the submitting provider is still
mounted. If a provider write is part of the mutation itself, keep it inside the
run(tx, ...) callback instead of afterSuccess.
Direct Runner Usage #
If you are not using riverpod_annotation, use MutationRunner directly:
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
final saveCounterMutation = Mutation<int>();
final counterSaveControllerProvider =
NotifierProvider<CounterSaveController, int>(CounterSaveController.new);
class CounterSaveController extends Notifier<int> {
final _runner = MutationRunner<int>();
@override
int build() => 0;
Future<int> save() {
return _runner.submitAction(
ref,
saveCounterMutation,
(tx) async {
final next = state + 1;
state = next;
return next;
},
);
}
}
See example/manual_runner_example.dart.
Manual Usage With riverpod_annotation #
This is still Riverpod codegen, but the mutation wiring is handwritten:
Non-family:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
part 'manual_annotation_non_family_example.g.dart';
final counterSaveMutation = Mutation<int>();
@riverpod
class ManualCounterSave extends _$ManualCounterSave
with StateFormMixin<int, int> {
@override
int build() => 0;
@override
Mutation<int> get mutationBase => counterSaveMutation;
Future<int> save() {
return submit((tx, form) async => form + 1);
}
}
See example/manual_annotation_non_family_example.dart.
Family:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
part 'manual_annotation_example.g.dart';
final itemUpdateFormMutationBase = Mutation<String>();
Mutation<String> itemUpdateFormMutation(String id) {
return itemUpdateFormMutationBase(id);
}
@riverpod
class ManualItemUpdateForm extends _$ManualItemUpdateForm
with StateFormMixin<String, String> {
@override
String build(String id) => id;
@override
Mutation<String> get mutationBase => itemUpdateFormMutationBase;
@override
Object get mutationKey => id;
Future<String> save() {
return submit((tx, form) async {
return 'saved:$form';
});
}
}
See example/manual_annotation_example.dart.
Generated Usage #
The companion generator package can generate mutationBase, mutationKey, and
a public mutation accessor for you:
Non-family:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
part 'generated_non_family_example.g.dart';
@generateMutation
@riverpod
class GeneratedCounterSave extends _$GeneratedCounterSave
with StateFormMixin<int, int>, _$GeneratedCounterSaveMutation {
@override
int build() => 0;
Future<int> save() {
return submit((tx, form) async => form + 1);
}
}
See example/generated_non_family_example.dart.
Family:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
part 'item_update_form.g.dart';
@generateMutation
@riverpod
class ItemUpdateForm extends _$ItemUpdateForm
with StateFormMixin<int, String>, _$ItemUpdateFormMutation {
@override
int build(String id) => 0;
}
That generated mixin wires the correct keyed mutation automatically, and the
generated top-level itemUpdateFormMutation(...) accessor can be watched from
the UI.
See example/riverpod_mutation_utils_example.dart.
When a family has multiple parameters, the generated mutationKey becomes a
record of those arguments, so each parameter combination gets isolated mutation
state. See example/generated_multi_param_example.dart.
submit(...) keeps the submitting provider alive while the mutation is pending,
which makes afterSuccess safe to use with ref for the common auto-dispose
case. If the provider is explicitly invalidated or rebuilt before completion,
the original ref becomes unmounted and afterSuccess is skipped.
Design Notes #
run(tx, ...)is the only place whereMutationTransactionis guaranteed to be valid.afterSuccessandafterErrorare post-transaction hooks.- Family providers must use keyed mutations. Use
mutationKeymanually or prefer@generateMutationso the key is derived automatically. - Multiple family parameters are keyed as a Dart record.
- Mutation state is transient. Watch the mutation if the UI needs to reflect pending, success, or error states.
API #
MutationRunner<Result>StateFormMixin<FormState, Result>AsyncStateFormMixin<FormState, Result>MutationActionMixin<Result>