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
Section titled “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
Section titled “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
Section titled “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
Section titled “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()
Section titled “2. In .mutate()”Use this for UI-specific actions like Navigation or SnackBars.
cubit.mutate( 'Jane', onSuccess: (user) => Navigator.of(context).pop(),);Optimistic Updates
Section titled “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
Section titled “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
Section titled “Resetting State”Reset mutation state to idle to clear errors or data (e.g., when closing a dialog).
context.read<CreateUserMutationCubit>().reset();Next Steps
Section titled “Next Steps”- QueryCubit - Learn about queries
- Composition - Manage multiple queries
- Examples - Complete working examples