Skip to content

MutationCubit

The MutationCubit is an abstract base class for performing server-side mutations, emitting MutationState changes. Extend it to create cubits that handle operations that modify data on the server.

Version 0.3.0+ adds powerful lifecycle hooks for side effects and optimistic updates.

Extend MutationCubit and implement the required getter:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fasq_bloc/fasq_bloc.dart';
class CreateUserMutationCubit extends MutationCubit<User, String> {
@override
Future<User> Function(String variables) get mutationFn =>
(name) => api.createUser(name);
}

Call .mutate() with variables. You can optionally provide lifecycle callbacks directly here.

context.read<CreateUserMutationCubit>().mutate(
'John Doe',
onSuccess: (user) {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Welcome, ${user.name}!')),
);
},
onError: (error, context) {
print('Failed: $error');
}
);

Mutations often require side effects (logging, navigation, invalidation). You can define these in two places:

  1. In options (Global defaults for this Cubit)
  2. In .mutate() (Specific to the call site)

Use this for things that always happen, like cache invalidation.

class CreateUserMutationCubit extends MutationCubit<User, String> {
// ... mutationFn ...
@override
MutationOptions<User, String>? get options => MutationOptions(
onSuccess: (user) {
// Always invalidate the list when a user is created
QueryClient().invalidateQueries('users');
},
);
}

Use this for UI-specific actions like Navigation or SnackBars.

cubit.mutate(
'Jane',
onSuccess: (user) => Navigator.of(context).pop(),
);

Update the UI before the server responds.

cubit.mutate(
'New Task',
onMutate: () async {
// 1. Cancel background refetches to avoid overwriting our optimistic update
await QueryClient().cancelQueries('todos');
// 2. Snapshot the previous value
final previousTodos = QueryClient().getQueryData<List<Todo>>('todos');
// 3. Optimistically update the cache
QueryClient().setQueryData<List<Todo>>(
'todos',
[...?previousTodos, Todo(id: 'temp', title: 'New Task')]
);
// 4. Return context for potential rollback
return { 'previousTodos': previousTodos };
},
onError: (error, context) {
// 5. Rollback on error
if (context != null) {
QueryClient().setQueryData('todos', context['previousTodos']);
}
},
onSettled: () {
// 6. Always refetch to ensure we are in sync with server
QueryClient().invalidateQueries('todos');
}
);

Handle different mutation statuses:

BlocBuilder<CreateUserMutationCubit, MutationState<User>>(
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
if (state.hasError) return Text('Error: ${state.error}');
return ElevatedButton(
onPressed: () => context.read<CreateUserMutationCubit>().mutate('John'),
child: Text('Create'),
);
},
)

Reset mutation state to idle to clear errors or data (e.g., when closing a dialog).

context.read<CreateUserMutationCubit>().reset();