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.
Basic Usage
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);
}Triggering Mutations
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');
}
);Lifecycle Hooks & Side Effects
Mutations often require side effects (logging, navigation, invalidation). You can define these in two places:
- In
options(Global defaults for this Cubit) - In
.mutate()(Specific to the call site)
1. In options
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');
},
);
}2. In .mutate()
Use this for UI-specific actions like Navigation or SnackBars.
cubit.mutate(
'Jane',
onSuccess: (user) => Navigator.of(context).pop(),
);Optimistic Updates
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');
}
);Status Handling
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'),
);
},
)Resetting State
Reset mutation state to idle to clear errors or data (e.g., when closing a dialog).
context.read<CreateUserMutationCubit>().reset();Next Steps
- QueryCubit - Learn about queries
- Composition - Manage multiple queries
- Examples - Complete working examples