Mutations (experimental)
Mutations are experimental, and the API may change in a breaking way without a major version bump.
Mutations, in Riverpod, are objects which enable the user interface
to react to state changes.
A common use-case is displaying a loading indicator while a form is being submitted
In short, mutations are to achieve effects such as this:
!
Without mutations, you would have to store the progress of the form submission directly inside the state of a provider. This is not ideal as it pollutes the state of your provider with UI concerns ; and it involves a lot of boilerplate code to handle the loading state, error state, and success state.
Mutations are designed to handle these concerns in a more elegant way.
Defining a mutation
Mutations are instances of the Mutation object, stored in a final variable somewhere.
// A mutation to track the "add todo" operation.
// The generic type is optional and can be specified to enable the UI to interact
// with the result of the mutation.
final addTodo = Mutation<Todo>();
Typically, this variable will either be global or as a static final
variable on
a Notifier.
Listening to a mutation
Once we've defined a mutation, we can start using it inside Consumers or Providers.
For this, we will need a Refs and pick a listening method of our choice
(typically Ref.watch).
A typical example would be:
class Example extends ConsumerWidget {
const Example({super.key});
Widget build(BuildContext context) {
// We listen to the current state of the "addTodo" mutation.
// Listening to this will not perform any side effects by itself.
final addTodoState = ref.watch(addTodo);
return Row(
children: [
ElevatedButton(
style: ButtonStyle(
// If there is an error, we show the button in red
backgroundColor: switch (addTodoState) {
MutationError() => WidgetStatePropertyAll(Colors.red),
_ => null,
},
),
onPressed: () {
// TODO
},
child: const Text('Add todo'),
),
// The operation is pending, let's show a progress indicator
if (addTodoState is MutationPending) ...[
const SizedBox(width: 8),
const CircularProgressIndicator(),
],
],
);
}
}
Triggering a mutation
So far, we've listened to the state of a mutation, but nothing actually happens yet.
To trigger a mutation, we can use Mutation.run, pass our mutation, and provide an asynchronous callback that updates whatever state we want. Lastly, we'll need to return a value matching the generic type of the mutation.
ElevatedButton(
onPressed: () {
// Trigger the mutation, and run the callback.
// During the callback, we obtain a MutationTransaction (tsx) object
// which we can use to access providers and perform operations.
addTodo.run(ref, (tsx) async {
// We use tsx.get to access providers within mutations.
// This will keep the provider alive for the duration of the operation.
final todoNotifier = tsx.get(todoNotifierProvider);
// We perform a perform request using a Notifier.
final createdTodo = await todoNotifier.addTodo('Eat a cookie');
// We return the created todo. This enables our UI to show information
// about the created todo, such as its ID/creation date/etc.
return createdTodo;
});
},
child: const Text('Add todo'),
);
The different mutation states and their meaning
Mutations can be in one of the following states:
- MutationPending: The mutation has started and is currently loading.
- MutationError: The mutation has failed, and an error is available.
- MutationSuccess: The mutation has succeeded, and the result is available.
- MutationIdle: The mutation has not been called yet, or has been reset.
You can switch over the different states using a switch
statement:
switch (addTodo.state) {
case MutationPending():
case MutationError():
case MutationSuccess():
case MutationIdle():
}
After a mutation has been started once, how to reset it to its idle state?
Mutations naturally reset themselves to MutationIdle if:
- They have completed (either successfully or with an error).
- All listeners have been removed (e.g. the spinner widget has been removed)
This is similar to how Automatic disposal works, but for mutations.
Alternatively, you can manually reset a mutation to its idle state by calling the Mutation.reset method:
ElevatedButton(
onPressed: () {
// Reset the mutation to its idle state.
addTodo.reset(ref);
},
child: const Text('Reset mutation'),
);