Bloc Adapter Examples
Complete working examples using the Fasq Bloc adapter.
Global Setup
Section titled “Global Setup”All examples assume your app is wrapped in FasqBlocProvider:
void main() { runApp( FasqBlocProvider( child: MaterialApp( home: UserManagementScreen(), ), ), );}Basic User Management
Section titled “Basic User Management”A complete example showing user listing, creation, and deletion:
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:fasq_bloc/fasq_bloc.dart';
class User { final String id; final String name; final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'], name: json['name'], email: json['email'], ); }}
class ApiService { Future<List<User>> fetchUsers() async { await Future.delayed(Duration(seconds: 1)); return [ User(id: '1', name: 'Alice', email: 'alice@example.com'), User(id: '2', name: 'Bob', email: 'bob@example.com'), ]; }
Future<User> createUser(Map<String, String> data) async { await Future.delayed(Duration(seconds: 1)); return User( id: DateTime.now().millisecondsSinceEpoch.toString(), name: data['name']!, email: data['email']!, ); }
Future<void> deleteUser(String id) async { await Future.delayed(Duration(seconds: 1)); }}
final api = ApiService();
class UsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users';
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();}
class CreateUserMutationCubit extends MutationCubit<User, Map<String, String>> { @override Future<User> Function(Map<String, String> variables) get mutationFn => (data) => api.createUser(data);
@override MutationOptions<User, Map<String, String>>? get options => MutationOptions( onSuccess: (user) { QueryClient().invalidateQuery('users'); }, );}
class DeleteUserMutationCubit extends MutationCubit<void, String> { @override Future<void> Function(String variables) get mutationFn => (id) => api.deleteUser(id);
@override MutationOptions<void, String>? get options => MutationOptions( onSuccess: () { QueryClient().invalidateQuery('users'); }, );}
class UserManagementScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('User Management')), body: MultiBlocProvider( providers: [ BlocProvider(create: (context) => UsersQueryCubit()), BlocProvider(create: (context) => CreateUserMutationCubit()), BlocProvider(create: (context) => DeleteUserMutationCubit()), ], child: Column( children: [ Expanded(child: UsersList()), Padding( padding: const EdgeInsets.all(16.0), child: ElevatedButton( onPressed: () => _showCreateUserDialog(context), child: Text('Add User'), ), ), ], ), ), ); }
void _showCreateUserDialog(BuildContext context) { showDialog( context: context, builder: (context) => CreateUserDialog(), ); }}
class UsersList extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder<UsersQueryCubit, QueryState<List<User>>>( builder: (context, state) { if (state.isLoading) { return Center(child: CircularProgressIndicator()); }
if (state.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Error: ${state.error}'), ElevatedButton( onPressed: () => context.read<UsersQueryCubit>().refetch(), child: Text('Retry'), ), ], ), ); }
if (state.hasData) { return ListView.builder( itemCount: state.data!.length, itemBuilder: (context, index) { final user = state.data![index]; return UserTile(user: user); }, ); }
return Center(child: Text('No users found')); }, ); }}
class UserTile extends StatelessWidget { final User user;
const UserTile({required this.user});
@override Widget build(BuildContext context) { return BlocBuilder<DeleteUserMutationCubit, MutationState<void>>( builder: (context, deleteState) { return ListTile( title: Text(user.name), subtitle: Text(user.email), trailing: IconButton( icon: Icon(Icons.delete), onPressed: deleteState.isLoading ? null : () { context.read<DeleteUserMutationCubit>().mutate(user.id); }, ), ); }, ); }}
class CreateUserDialog extends StatefulWidget { @override State<CreateUserDialog> createState() => _CreateUserDialogState();}
class _CreateUserDialogState extends State<CreateUserDialog> { final _nameController = TextEditingController(); final _emailController = TextEditingController();
@override Widget build(BuildContext context) { return BlocProvider( create: (context) => CreateUserMutationCubit(), child: BlocBuilder<CreateUserMutationCubit, MutationState<User>>( builder: (context, createState) { return AlertDialog( title: Text('Create User'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _nameController, decoration: InputDecoration(labelText: 'Name'), ), TextField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Cancel'), ), ElevatedButton( onPressed: createState.isLoading ? null : () { context.read<CreateUserMutationCubit>().mutate({ 'name': _nameController.text, 'email': _emailController.text, }); Navigator.pop(context); }, child: createState.isLoading ? CircularProgressIndicator() : Text('Create'), ), ], ); }, ), ); }}Infinite Queries Example
Section titled “Infinite Queries Example”Implementing infinite scroll with InfiniteQueryCubit:
class PostsInfiniteQueryCubit extends InfiniteQueryCubit<List<Post>, int> { @override String get key => 'posts';
@override Future<List<Post>> Function(int param) get queryFn => (page) => api.fetchPosts(page: page);
@override InfiniteQueryOptions<List<Post>, int>? get options => InfiniteQueryOptions( getNextPageParam: (pages, last) => pages.length + 1, );}
class PostsInfiniteScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Posts')), body: BlocProvider( create: (_) => PostsInfiniteQueryCubit(), child: BlocBuilder<PostsInfiniteQueryCubit, InfiniteQueryState<List<Post>, int>>( builder: (context, state) { final allPosts = state.pages.expand((p) => p.data ?? []).toList();
return Column( children: [ Expanded( child: ListView.builder( itemCount: allPosts.length, itemBuilder: (context, index) { return PostItem(allPosts[index]); }, ), ), if (state.hasNextPage) ElevatedButton( onPressed: () { context.read<PostsInfiniteQueryCubit>().fetchNextPage(); }, child: Text('Load More'), ), ], ); }, ), ), ); }}Real-time Updates
Section titled “Real-time Updates”Simulating real-time updates with polling:
class RealTimeUsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users:realtime';
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();
@override QueryOptions? get options => QueryOptions( staleTime: Duration(seconds: 30), );}
class RealTimeUsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Real-time Users'), actions: [ IconButton( icon: Icon(Icons.refresh), onPressed: () { context.read<RealTimeUsersQueryCubit>().refetch(); }, ), ], ), body: BlocProvider( create: (context) => RealTimeUsersQueryCubit(), child: BlocBuilder<RealTimeUsersQueryCubit, QueryState<List<User>>>( builder: (context, state) { return Column( children: [ if (state.isFetching && !state.isLoading) LinearProgressIndicator(), Expanded( child: state.when( idle: () => Center(child: Text('Ready to load users')), loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ), ), ), ], ); }, ), ), ); }}Optimistic Updates
Section titled “Optimistic Updates”Implementing optimistic updates for instant feedback:
class UpdateUserMutationCubit extends MutationCubit<User, User> { @override Future<User> Function(User variables) get mutationFn => (user) => api.updateUser(user);
@override MutationOptions<User, User>? get options => MutationOptions( onMutate: (updatedUser) { final users = QueryClient().getQueryData<List<User>>('users'); if (users != null) { final optimistic = users.map((u) => u.id == updatedUser.id ? updatedUser : u ).toList();
QueryClient().setQueryData('users', optimistic); } }, onSuccess: (user) { QueryClient().invalidateQuery('users'); }, onError: (error) { QueryClient().invalidateQuery('users'); }, );}
class OptimisticUserScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Optimistic Updates')), body: MultiBlocProvider( providers: [ BlocProvider(create: (context) => UsersQueryCubit()), BlocProvider(create: (context) => UpdateUserMutationCubit()), ], child: BlocBuilder<UsersQueryCubit, QueryState<List<User>>>( builder: (context, usersState) { return usersState.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserTileWithUpdate(user: user); }, ), ); }, ), ), ); }}
class UserTileWithUpdate extends StatelessWidget { final User user;
const UserTileWithUpdate({required this.user});
@override Widget build(BuildContext context) { return BlocBuilder<UpdateUserMutationCubit, MutationState<User>>( builder: (context, updateState) { return ListTile( title: Text(user.name), subtitle: Text(user.email), trailing: IconButton( icon: Icon(Icons.edit), onPressed: updateState.isLoading ? null : () { final updatedUser = User( id: user.id, name: '${user.name} (Updated)', email: user.email, ); context.read<UpdateUserMutationCubit>().mutate(updatedUser); }, ), ); }, ); }}Search Functionality
Section titled “Search Functionality”Implementing search with parameterized queries:
class SearchResultsQueryCubit extends QueryCubit<List<User>> { final String query;
SearchResultsQueryCubit(this.query);
@override String get key => 'search:$query';
@override Future<List<User>> Function() get queryFn => () => api.searchUsers(query);
@override QueryOptions? get options => QueryOptions( enabled: query.isNotEmpty && query.length >= 2, staleTime: Duration(minutes: 5), );}
class SearchScreen extends StatefulWidget { @override State<SearchScreen> createState() => _SearchScreenState();}
class _SearchScreenState extends State<SearchScreen> { final _searchController = TextEditingController(); String _searchQuery = '';
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Search Users')), body: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: TextField( controller: _searchController, decoration: InputDecoration( labelText: 'Search users', prefixIcon: Icon(Icons.search), ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), ), Expanded( child: BlocProvider( create: (context) => SearchResultsQueryCubit(_searchQuery), child: BlocBuilder<SearchResultsQueryCubit, QueryState<List<User>>>( builder: (context, searchState) { return searchState.when( idle: () => Center(child: Text('Enter a search term')), loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (results) => results.isEmpty ? Center(child: Text('No results found')) : ListView.builder( itemCount: results.length, itemBuilder: (context, index) { final user = results[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ), ); }, ), ), ), ], ), ); }}Form Handling
Section titled “Form Handling”Complete form handling with validation:
class CreateUserFormMutationCubit extends MutationCubit<User, Map<String, String>> { @override Future<User> Function(Map<String, String> variables) get mutationFn => (data) => api.createUser(data);
@override MutationOptions<User, Map<String, String>>? get options => MutationOptions( onSuccess: (user) { QueryClient().invalidateQuery('users'); }, );}
class CreateUserForm extends StatefulWidget { @override State<CreateUserForm> createState() => _CreateUserFormState();}
class _CreateUserFormState extends State<CreateUserForm> { final _formKey = GlobalKey<FormState>(); final _nameController = TextEditingController(); final _emailController = TextEditingController();
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Create User')), body: BlocProvider( create: (context) => CreateUserFormMutationCubit(), child: BlocBuilder<CreateUserFormMutationCubit, MutationState<User>>( builder: (context, mutationState) { return Form( key: _formKey, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ TextFormField( controller: _nameController, decoration: InputDecoration(labelText: 'Name'), validator: (value) { if (value == null || value.isEmpty) { return 'Name is required'; } return null; }, ), SizedBox(height: 16), TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Email is required'; } return null; }, ), SizedBox(height: 24), ElevatedButton( onPressed: mutationState.isLoading ? null : () { if (_formKey.currentState!.validate()) { context.read<CreateUserFormMutationCubit>().mutate({ 'name': _nameController.text, 'email': _emailController.text, }); _nameController.clear(); _emailController.clear(); } }, child: mutationState.isLoading ? CircularProgressIndicator() : Text('Create User'), ), ], ), ), ); }, ), ), ); }}Performance Optimization
Section titled “Performance Optimization”Optimizing performance with proper cache configuration:
class OptimizedUsersQueryCubit extends QueryCubit<List<User>> { @override String get key => 'users';
@override Future<List<User>> Function() get queryFn => () => api.fetchUsers();
@override QueryOptions? get options => QueryOptions( staleTime: Duration(minutes: 10), cacheTime: Duration(minutes: 30), refetchOnMount: false, );}
class OptimizedUsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Optimized Users')), body: BlocProvider( create: (context) => OptimizedUsersQueryCubit(), child: BlocBuilder<OptimizedUsersQueryCubit, QueryState<List<User>>>( builder: (context, state) { return state.when( loading: () => Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), data: (users) => ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return ListTile( title: Text(user.name), subtitle: Text(user.email), ); }, ), ); }, ), ), ); }}Next Steps
Section titled “Next Steps”- QueryCubit - Learn about the QueryCubit
- MutationCubit - Learn about the MutationCubit
- InfiniteQueryCubit - Learn about infinite queries
- Bloc Patterns - Best practices and patterns
- Testing - Testing strategies