Bloc Patterns
Best practices and common patterns for using Fasq with Bloc. These patterns will help you build maintainable and performant applications using the Bloc architecture.
Cubit Organization
Section titled “Cubit Organization”Group Related Cubits
Section titled “Group Related Cubits”Organize cubits by feature or domain:
class UsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users';
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();}
class UserQueryCubit extends QueryCubit<User> { final String userId;
UserQueryCubit(this.userId);
@override String get key => 'user:$userId';
@override Future<User> Function() get queryFn => () => api.fetchUser(userId);}
class CreateUserMutationCubit extends MutationCubit<User, Map<String, String>> { @override Future<User> Function(Map<String, String> variables) get mutationFn => (data) => api.createUser(data);}
class UpdateUserMutationCubit extends MutationCubit<User, User> { @override Future<User> Function(User variables) get mutationFn => (user) => api.updateUser(user);}
class DeleteUserMutationCubit extends MutationCubit<void, String> { @override Future<void> Function(String variables) get mutationFn => (userId) => api.deleteUser(userId);}Cubit Dependencies
Section titled “Cubit Dependencies”Create cubits that depend on other cubits:
class AuthQueryCubit extends QueryCubit<User?> { @override String get key => 'auth';
@override Future<User?> Function() get queryFn => () => api.getCurrentUser();}
class UserPostsQueryCubit extends QueryCubit<List<Post>> { final String userId;
UserPostsQueryCubit(this.userId);
@override String get key => 'posts:user:$userId';
@override Future<List<Post>> Function() get queryFn => () => api.fetchUserPosts(userId);
@override QueryOptions? get options => QueryOptions( enabled: () { final authCubit = QueryClient().getQueryByKey<User?>('auth'); return authCubit?.state.hasData == true && authCubit?.state.data != null; }, );}State Management Patterns
Section titled “State Management Patterns”Combining Multiple Cubits
Section titled “Combining Multiple Cubits”For complex screens requiring data from multiple sources, you have two main options:
1. Composition with Mixin (Recommended)
Section titled “1. Composition with Mixin (Recommended)”Use FasqSubscriptionMixin to subscribe to multiple queries within a single Bloc and emit a unified state. This keeps your business logic testable and your UI clean.
See the Composition Guide for details.
class DashboardCubit extends Cubit<DashboardState> with FasqSubscriptionMixin { DashboardCubit() : super(DashboardState.initial()) { final userQuery = client.getQuery<User>('user'.toQueryKey(), ...); final postsQuery = client.getQuery<List<Post>>('posts'.toQueryKey(), ...);
subscribeToQuery(userQuery, (state) { emit(state.copyWith(user: state.data)); });
subscribeToQuery(postsQuery, (state) { emit(state.copyWith(posts: state.data)); }); }}2. Widget Composition (Simple)
Section titled “2. Widget Composition (Simple)”For simple layouts where independent widgets need independent data, use nested BlocBuilders or MultiBlocProvider:
Derived State
Section titled “Derived State”Create computed state from other cubits:
class UsersStatsCubit extends Cubit<UsersStats> { UsersStatsCubit() : super(UsersStats.empty());
void updateStats(List<User> users) { emit(UsersStats( totalUsers: users.length, activeUsers: users.where((user) => user.isActive).length, inactiveUsers: users.where((user) => !user.isActive).length, )); }}
class UsersStats { final int totalUsers; final int activeUsers; final int inactiveUsers;
UsersStats({ required this.totalUsers, required this.activeUsers, required this.inactiveUsers, });
factory UsersStats.empty() => UsersStats( totalUsers: 0, activeUsers: 0, inactiveUsers: 0, );}
class UsersScreenWithStats extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider(create: (context) => UsersQueryCubit()), BlocProvider(create: (context) => UsersStatsCubit()), ], child: BlocListener<UsersQueryCubit, QueryState<List<User>>>( listener: (context, state) { if (state.hasData) { context.read<UsersStatsCubit>().updateStats(state.data!); } }, child: BlocBuilder<UsersStatsCubit, UsersStats>( builder: (context, stats) { return Column( children: [ Text('Total Users: ${stats.totalUsers}'), Text('Active Users: ${stats.activeUsers}'), Text('Inactive Users: ${stats.inactiveUsers}'), ], ); }, ), ), ); }}Error Handling Patterns
Section titled “Error Handling Patterns”Global Error Handler
Section titled “Global Error Handler”Create a global error handler for mutations:
class GlobalErrorHandler { static void handleError(BuildContext context, Object error) { // Log error print('Global error: $error');
// Show error snackbar ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('An error occurred: $error'), backgroundColor: Colors.red, ), ); }}
class CreateUserMutationCubit extends MutationCubit<User, Map<String, String>> { CreateUserMutationCubit() : super( mutationFn: (data) => api.createUser(data), options: MutationOptions( onError: (error) { // Handle error globally GlobalErrorHandler.handleError(context, error); }, ), );}Retry Logic
Section titled “Retry Logic”Implement retry logic for failed queries:
class UsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users';
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();
@override QueryOptions? get options => QueryOptions( onError: (error) { Future.delayed(Duration(seconds: 2), () { refetch(); }); }, );}Cache Management Patterns
Section titled “Cache Management Patterns”Cache Warming
Section titled “Cache Warming”Preload data for better performance:
class CacheWarmerCubit extends Cubit<void> { CacheWarmerCubit() : super(null);
void warmCache() { // Warm cache on app start QueryClient().prefetchQuery('users', () => api.fetchUsers()); QueryClient().prefetchQuery('posts', () => api.fetchPosts()); }}
class AppInitializer extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CacheWarmerCubit()..warmCache(), child: MaterialApp( home: HomeScreen(), ), ); }}Cache Invalidation Strategies
Section titled “Cache Invalidation Strategies”Implement smart cache invalidation:
class CacheInvalidationService { static void invalidateUserCache(String userId) { // Invalidate specific user QueryClient().invalidateQuery('user:$userId');
// Invalidate user list if it might be affected QueryClient().invalidateQuery('users');
// Invalidate user posts QueryClient().invalidateQuery('posts:user:$userId'); }}
class UpdateUserMutationCubit extends MutationCubit<User, User> { UpdateUserMutationCubit() : super( mutationFn: (user) => api.updateUser(user), options: MutationOptions( onSuccess: (updatedUser) { CacheInvalidationService.invalidateUserCache(updatedUser.id); }, ), );}Form Handling Patterns
Section titled “Form Handling Patterns”Form State Management
Section titled “Form State Management”Manage form state with Bloc:
class CreateUserFormCubit extends Cubit<CreateUserFormState> { CreateUserFormCubit() : super(CreateUserFormState.initial());
void updateName(String name) { emit(state.copyWith( name: name, isValid: name.isNotEmpty && state.email.isNotEmpty, nameError: name.isEmpty ? 'Name is required' : null, )); }
void updateEmail(String email) { emit(state.copyWith( email: email, isValid: state.name.isNotEmpty && email.isNotEmpty, emailError: email.isEmpty ? 'Email is required' : null, )); }
void reset() { emit(CreateUserFormState.initial()); }}
class CreateUserFormState { final String name; final String email; final bool isValid; final String? nameError; final String? emailError;
CreateUserFormState({ required this.name, required this.email, required this.isValid, this.nameError, this.emailError, });
factory CreateUserFormState.initial() => CreateUserFormState( name: '', email: '', isValid: false, );
CreateUserFormState copyWith({ String? name, String? email, bool? isValid, String? nameError, String? emailError, }) { return CreateUserFormState( name: name ?? this.name, email: email ?? this.email, isValid: isValid ?? this.isValid, nameError: nameError, emailError: emailError, ); }}Form Submission with Validation
Section titled “Form Submission with Validation”class CreateUserForm extends StatelessWidget { @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider(create: (context) => CreateUserFormCubit()), BlocProvider(create: (context) => CreateUserMutationCubit()), ], child: BlocBuilder<CreateUserFormCubit, CreateUserFormState>( builder: (context, formState) { return BlocBuilder<CreateUserMutationCubit, MutationState<User>>( builder: (context, mutationState) { return Form( child: Column( children: [ TextFormField( decoration: InputDecoration(labelText: 'Name'), onChanged: (value) { context.read<CreateUserFormCubit>().updateName(value); }, validator: (value) => formState.nameError, ), TextFormField( decoration: InputDecoration(labelText: 'Email'), onChanged: (value) { context.read<CreateUserFormCubit>().updateEmail(value); }, validator: (value) => formState.emailError, ), ElevatedButton( onPressed: formState.isValid && !mutationState.isLoading ? () { final mutationCubit = context.read<CreateUserMutationCubit>(); mutationCubit.mutate({ 'name': formState.name, 'email': formState.email, }); } : null, child: mutationState.isLoading ? CircularProgressIndicator() : Text('Create User'), ), ], ), ); }, ); }, ), ); }}Navigation Patterns
Section titled “Navigation Patterns”Route-Based Data Loading
Section titled “Route-Based Data Loading”Load data based on route parameters:
class UserDetailScreen extends StatelessWidget { final String userId;
const UserDetailScreen({required this.userId});
@override Widget build(BuildContext context) { return BlocProvider( create: (context) => UserQueryCubit(userId), child: Scaffold( appBar: AppBar(title: Text('User Details')), body: BlocBuilder<UserQueryCubit, QueryState<User>>( builder: (context, state) { if (state.isLoading) return CircularProgressIndicator(); if (state.hasError) return Text('Error: ${state.error}'); if (state.hasData) return UserDetailsView(user: state.data!); return SizedBox(); }, ), ), ); }}Deep Linking Support
Section titled “Deep Linking Support”Handle deep links with proper data loading:
class DeepLinkHandler { static void handleDeepLink(BuildContext context, String path) { final segments = path.split('/');
if (segments.length >= 2) { final resource = segments[1]; final id = segments.length >= 3 ? segments[2] : null;
switch (resource) { case 'users': if (id != null) { QueryClient().prefetchQuery('user:$id', () => api.fetchUser(id)); } else { QueryClient().prefetchQuery('users', () => api.fetchUsers()); } break; case 'posts': if (id != null) { QueryClient().prefetchQuery('post:$id', () => api.fetchPost(id)); } else { QueryClient().prefetchQuery('posts', () => api.fetchPosts()); } break; } } }}Testing Patterns
Section titled “Testing Patterns”Mock Cubits for Testing
Section titled “Mock Cubits for Testing”Create mock cubits for testing:
class MockUsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users';
@override Future<List<User>> Function() get queryFn => () => Future.value([ User(id: '1', name: 'Test User 1'), User(id: '2', name: 'Test User 2'), ]);}
class MockCreateUserMutationCubit extends MutationCubit<User, Map<String, String>> { @override Future<User> Function(Map<String, String> variables) get mutationFn => (data) => Future.value(User( id: '3', name: data['name']!, email: data['email']!, ));}
// In your testvoid main() { testWidgets('should display users', (tester) async { await tester.pumpWidget( MultiBlocProvider( providers: [ BlocProvider<QueryCubit<List<User>>, QueryState<List<User>>>( create: (context) => MockUsersQueryCubit(), ), ], child: MaterialApp(home: UsersScreen()), ), );
await tester.pumpAndSettle();
expect(find.text('Test User 1'), findsOneWidget); expect(find.text('Test User 2'), findsOneWidget); });}Cubit Testing Utilities
Section titled “Cubit Testing Utilities”Create utilities for testing cubits:
class CubitTestHelper { static Future<void> pumpWidgetWithCubits( WidgetTester tester, Widget child, List<BlocProvider> providers, ) async { await tester.pumpWidget( MultiBlocProvider( providers: providers, child: MaterialApp(home: child), ), ); }
static Future<void> waitForCubitState<T extends Cubit<S>, S>( WidgetTester tester, T cubit, S expectedState, ) async { await tester.pump(); while (cubit.state != expectedState) { await tester.pump(Duration(milliseconds: 100)); } }}
// Usage in testsvoid main() { testWidgets('should create user successfully', (tester) async { final createUserCubit = MockCreateUserMutationCubit();
await CubitTestHelper.pumpWidgetWithCubits( tester, CreateUserForm(), [ BlocProvider<MutationCubit<User, Map<String, String>>, MutationState<User>>( create: (context) => createUserCubit, ), ], );
await tester.enterText(find.byType(TextFormField).first, 'Test User'); await tester.enterText(find.byType(TextFormField).last, 'test@example.com'); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle();
expect(createUserCubit.state.isSuccess, isTrue); expect(createUserCubit.state.data?.name, equals('Test User')); });}Performance Optimization Patterns
Section titled “Performance Optimization Patterns”Lazy Loading
Section titled “Lazy Loading”Implement lazy loading for large datasets:
class PaginatedUsersCubit extends Cubit<List<User>> { PaginatedUsersCubit() : super([]);
Future<void> loadPage(int page) async { final users = await api.fetchUsersPage(page: page, limit: 20); emit([...state, ...users]); }
Future<void> loadAllPages() async { int currentPage = 1; List<User> allUsers = [];
while (true) { final users = await api.fetchUsersPage(page: currentPage, limit: 20); allUsers.addAll(users);
if (users.length < 20) break; // Last page currentPage++; }
emit(allUsers); }}Debounced Search
Section titled “Debounced Search”Implement debounced search to avoid excessive API calls:
class SearchQueryCubit extends Cubit<String> { Timer? _debounceTimer;
SearchQueryCubit() : super('');
void updateQuery(String query) { _debounceTimer?.cancel(); _debounceTimer = Timer(Duration(milliseconds: 500), () { emit(query); }); }
@override Future<void> close() { _debounceTimer?.cancel(); return super.close(); }}
class SearchResultsCubit extends QueryCubit<List<Post>> { SearchResultsCubit(String query) : super( queryKey: 'search:$query', queryFn: () => api.searchPosts(query), options: QueryOptions( enabled: query.isNotEmpty && query.length >= 2, ), );}Best Practices
Section titled “Best Practices”- Organize cubits by feature - Keep related cubits together
- Use meaningful names - Make cubit names descriptive
- Handle errors gracefully - Implement proper error handling
- Optimize cache usage - Use appropriate staleTime and cacheTime
- Test thoroughly - Create comprehensive tests for cubits
- Use type safety - Leverage generic types for compile-time safety
- Document complex logic - Add comments for complex cubit logic
Next Steps
Section titled “Next Steps”- QueryCubit - Learn about the QueryCubit
- MutationCubit - Learn about the MutationCubit
- Testing - Testing strategies
- Examples - Complete working examples