riverpod_mutation_utils 0.5.5
riverpod_mutation_utils: ^0.5.5 copied to clipboard
Runtime helpers, annotations, and mixins for Riverpod experimental mutations.
riverpod_mutation_utils #
Runtime helpers, annotations, and mixins for Riverpod experimental mutations.
This package extracts the non-UI mutation layer so multiple apps can share:
- configurable mutation reset 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.5.5
If you use riverpod_annotation, also add:
dependencies:
riverpod_annotation: ^4.0.3
dev_dependencies:
build_runner: ^2.7.1
riverpod_generator: ^4.0.4
If you want generated mutation wiring, also add:
dev_dependencies:
riverpod_mutation_utils_generator: ^0.5.5
Quick Start #
Pick one integration level:
MutationRunnerif you are not usingriverpod_annotationStateFormMixin/AsyncStateFormMixinif you want handwritten mutation wiringMutationActionMixinif you want an action-only provider with no own state@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.
Action-only providers should return void from build() and expose mutation
progress by watching the separate Mutation<Result>. They stay alive while a
submission is pending, but do not automatically reset the mutation on provider
dispose. Reset them explicitly with mutation.reset(ref) when the UI or a
listener decides the transient state is no longer needed.
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;
},
);
}
}
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 mutation => counterSaveMutation;
Future<int> save() {
return submit((tx, form) async => form + 1);
}
}
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 mutation => itemUpdateFormMutation(id);
Future<String> save() {
return submit((tx, form) async {
return 'saved:$form';
});
}
}
Action-only:
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_mutation_utils/riverpod_mutation_utils.dart';
part 'manual_action_example.g.dart';
final counterSaveMutation = Mutation<int>();
@riverpod
class ManualCounterAction extends _$ManualCounterAction
with MutationActionMixin<int> {
@override
void build() {}
@override
Mutation<int> get mutation => counterSaveMutation;
Future<int> save() {
return submitAction((tx) async => 1);
}
}
Generated Usage #
The companion generator package can generate a stable mutation base, a keyed mutation accessor, and a convenience abstract base 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 _$GeneratedCounterSaveMutation
with StateFormMixin<int, int> {
@override
int build() => 0;
Future<int> save() {
return submit((tx, form) async => form + 1);
}
}
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 _$ItemUpdateFormMutation
with StateFormMixin<int, String> {
@override
int build(String id) => 0;
}
That generated base hides the wiring mixin while keeping
StateFormMixin<...> explicit, and the generated top-level
itemUpdateFormMutation(...) accessor can be watched from the UI.
When a family has multiple parameters, the generated accessor keys mutations by a Dart record of those arguments, so each parameter combination gets isolated mutation state.
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.
Form mixins keep the previous default of resetting their mutation on owner
dispose. MutationActionMixin intentionally does not.
Design Notes #
run(tx, ...)is the only place whereMutationTransactionis guaranteed to be valid.afterSuccessandafterErrorare post-transaction hooks.MutationActionMixinis forNotifier<void>providers only.MutationResetPolicy.onOwnerDisposeis the default for forms and directMutationRunner()usage.MutationResetPolicy.manualis the default forMutationActionMixin.- Family providers must return a keyed
mutation. Prefer@generateMutationso the accessor 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>MutationResetPolicyStateFormMixin<FormState, Result>AsyncStateFormMixin<FormState, Result>MutationActionMixin<Result>