Skip to content

Bloc Adapter Examples

Complete working examples using the Fasq Bloc adapter.

All examples assume your app is wrapped in FasqBlocProvider:

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

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'),
),
],
);
},
),
);
}
}

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'),
),
],
);
},
),
),
);
}
}

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),
);
},
),
),
),
],
);
},
),
),
);
}
}

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);
},
),
);
},
);
}
}

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),
);
},
),
);
},
),
),
),
],
),
);
}
}

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'),
),
],
),
),
);
},
),
),
);
}
}

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),
);
},
),
);
},
),
),
);
}
}