riverpod_mutation_utils 0.5.1
riverpod_mutation_utils: ^0.5.1 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.5.1
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.5.1
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>.
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 mutation => 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 mutation => itemUpdateFormMutation(id);
Future<String> save() {
return submit((tx, form) async {
return 'saved:$form';
});
}
}
See example/manual_annotation_example.dart.
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
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);
}
}
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 _$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.
See example/riverpod_mutation_utils_example.dart.
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. 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.MutationActionMixinis forNotifier<void>providers only. Non-family providers can rely on its default emptybuild(), while family providers still declarebuild(...)to expose their parameters.- 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>StateFormMixin<FormState, Result>AsyncStateFormMixin<FormState, Result>MutationActionMixin<Result>