Skip to Content

Examples

Complete working examples using the Fasq Bloc adapter.

Global Setup

All examples assume your app is wrapped in FasqBlocProvider:

void main() { runApp( FasqBlocProvider( child: MaterialApp( home: UserManagementScreen(), ), ), ); }

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

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

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

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

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

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

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

Last updated on