Ana içeriğe atla

Mutations (experimental)

uyarı

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:
Submit progress indicator!

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>();
not

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'),
);