fasq_bloc 0.2.0+1
fasq_bloc: ^0.2.0+1 copied to clipboard
Bloc/Cubit adapter for FASQ (Flutter Async State Query) - async state management with Bloc
fasq_bloc #
Bloc/Cubit adapter for FASQ (Flutter Async State Query) - bringing powerful async state management to your Bloc-based Flutter apps.
Features #
- π§ QueryCubit - Cubit wrapper for queries
- βΎοΈ InfiniteQueryCubit - Infinite queries for pagination
- π MutationCubit - Cubit for server mutations
- π MultiQueryBuilder - Execute multiple queries in parallel
- π Automatic caching - Built on FASQ's production-ready cache
- β‘ Background refetching - Stale-while-revalidate pattern
- π― Type-safe - Full type safety with Bloc
Installation #
dependencies:
fasq_bloc: ^0.1.0
Usage #
Infinite Queries with InfiniteQueryCubit #
BlocProvider(
create: (_) => InfiniteQueryCubit<List<Post>, int>(
key: 'posts',
queryFn: (page) => api.fetchPosts(page: page),
options: InfiniteQueryOptions(
getNextPageParam: (pages, last) => pages.length + 1,
),
),
child: BlocBuilder<InfiniteQueryCubit<List<Post>, int>, InfiniteQueryState<List<Post>, int>>(
builder: (context, state) {
return ListView.builder(
itemCount: state.pages.expand((p) => p.data ?? []).length,
itemBuilder: (_, i) => Text('Item #$i'),
);
},
),
)
Parallel Queries with MultiQueryBuilder #
Execute multiple queries in parallel using MultiQueryBuilder or NamedMultiQueryBuilder:
// Index-based access
MultiQueryBuilder(
configs: [
MultiQueryConfig(key: 'users', queryFn: () => api.fetchUsers()),
MultiQueryConfig(key: 'posts', queryFn: () => api.fetchPosts()),
MultiQueryConfig(key: 'comments', queryFn: () => api.fetchComments()),
],
builder: (context, state) {
return Column(
children: [
if (!state.isAllSuccess) LinearProgressIndicator(),
if (state.hasAnyError) ErrorBanner(),
UsersList(state.getState<List<User>>(0)),
PostsList(state.getState<List<Post>>(1)),
CommentsList(state.getState<List<Comment>>(2)),
],
);
},
)
// Named access (better DX)
NamedMultiQueryBuilder(
configs: [
NamedQueryConfig(name: 'users', key: 'users', queryFn: () => api.fetchUsers()),
NamedQueryConfig(name: 'posts', key: 'posts', queryFn: () => api.fetchPosts()),
NamedQueryConfig(name: 'comments', key: 'comments', queryFn: () => api.fetchComments()),
],
builder: (context, state) {
return Column(
children: [
if (!state.isAllSuccess) LinearProgressIndicator(),
if (state.hasAnyError) ErrorBanner(),
UsersList(state.getState<List<User>>('users')),
PostsList(state.getState<List<Post>>('posts')),
CommentsList(state.getState<List<Comment>>('comments')),
],
);
},
)
Prefetching #
Warm the cache before data is needed:
// Using PrefetchBuilder
PrefetchBuilder(
configs: [
PrefetchConfig(key: 'users', queryFn: () => api.fetchUsers()),
PrefetchConfig(key: 'posts', queryFn: () => api.fetchPosts()),
],
child: YourScreen(),
)
// Using PrefetchQueryCubit directly
final prefetchCubit = PrefetchQueryCubit();
await prefetchCubit.prefetch('users', () => api.fetchUsers());
Dependent Queries #
final userCubit = QueryCubit<User>(key: 'user', queryFn: fetchUser);
final postsCubit = QueryCubit<List<Post>>(
key: 'posts:user:${userCubit.state.data?.id}',
queryFn: () => fetchPosts(userCubit.state.data!.id),
options: const QueryOptions(enabled: false),
);
// Enable when user loaded
if (userCubit.state.isSuccess) {
// You can recreate with enabled true or structure initialization post user
}
Basic Query with QueryCubit #
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fasq_bloc/fasq_bloc.dart';
class UsersScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => QueryCubit<List<User>>(
key: 'users',
queryFn: () => api.fetchUsers(),
options: QueryOptions(
staleTime: Duration(minutes: 5),
),
),
child: BlocBuilder<QueryCubit<List<User>>, QueryState<List<User>>>(
builder: (context, state) {
if (state.isLoading) {
return CircularProgressIndicator();
}
if (state.hasError) {
return Text('Error: ${state.error}');
}
if (state.hasData) {
return UserList(users: state.data!);
}
return SizedBox();
},
),
);
}
}
Manual Refetch #
class UserList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: () {
// Refetch the query
context.read<QueryCubit<List<User>>>().refetch();
},
child: Text('Refresh'),
),
// ... list content
],
);
}
}
Mutations with MutationCubit #
class CreateUserScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => MutationCubit<User, String>(
mutationFn: (name) => api.createUser(name),
onSuccessCallback: (user) {
print('Created user: ${user.name}');
// Invalidate users query
QueryClient().invalidateQuery('users');
},
onErrorCallback: (error) {
print('Error: $error');
},
),
child: BlocBuilder<MutationCubit<User, String>, MutationState<User>>(
builder: (context, state) {
return Column(
children: [
if (state.isLoading)
CircularProgressIndicator(),
if (state.hasError)
Text('Error: ${state.error}'),
if (state.hasData)
Text('Created: ${state.data!.name}'),
ElevatedButton(
onPressed: state.isLoading
? null
: () {
context
.read<MutationCubit<User, String>>()
.mutate('John Doe');
},
child: Text('Create User'),
),
],
);
},
),
);
}
}
Cache Invalidation #
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
final cubit = context.read<QueryCubit<List<User>>>();
// Invalidate and refetch this query
cubit.invalidate();
// Or use QueryClient directly
QueryClient().invalidateQuery('users');
QueryClient().invalidateQueriesWithPrefix('user:');
},
child: Text('Invalidate Cache'),
);
}
}
API Reference #
QueryCubit #
class QueryCubit<T> extends Cubit<QueryState<T>> {
QueryCubit({
required String key,
required Future<T> Function() queryFn,
QueryOptions? options,
});
void refetch(); // Manually refetch
void invalidate(); // Invalidate and refetch
}
Emits: QueryState<T> with:
isLoading- Initial loading stateisFetching- Background refetch in progresshasData- Whether data is availabledata- The fetched datahasError- Whether an error occurrederror- The error object
MutationCubit #
class MutationCubit<TData, TVariables> extends Cubit<MutationState<TData>> {
MutationCubit({
required Future<TData> Function(TVariables) mutationFn,
void Function(TData)? onSuccessCallback,
void Function(Object)? onErrorCallback,
});
Future<void> mutate(TVariables variables);
void reset();
}
Emits: MutationState<TData> with:
isLoading- Whether mutation is in progressdata- Mutation resulterror- Mutation errorhasData- Whether mutation succeededhasError- Whether mutation failed
Why Bloc? #
If you're already using flutter_bloc, this adapter provides seamless integration with Flutter Query:
- Structured - Bloc's explicit state management
- Testable - Easy to test cubits
- Familiar - Use BlocBuilder/BlocConsumer as usual
- Debuggable - Bloc DevTools integration
Comparison with Core Package #
Core Package (QueryBuilder):
QueryBuilder<List<User>>(
queryKey: 'users',
queryFn: () => api.fetchUsers(),
builder: (context, state) {
if (state.isLoading) return Loading();
return UserList(state.data!);
},
)
Bloc Adapter (QueryCubit):
BlocProvider(
create: (_) => QueryCubit(
key: 'users',
queryFn: () => api.fetchUsers(),
),
child: BlocBuilder<QueryCubit<List<User>>, QueryState<List<User>>>(
builder: (context, state) {
if (state.isLoading) return Loading();
return UserList(state.data!);
},
),
)
Both approaches use the same underlying query engine and have identical performance.
Advanced Usage #
Using BlocConsumer for Side Effects #
BlocConsumer<QueryCubit<User>, QueryState<User>>(
listener: (context, state) {
if (state.hasError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${state.error}')),
);
}
if (state.isFetching) {
print('Background refresh in progress...');
}
},
builder: (context, state) {
// Build UI
},
)
Multiple Queries in One Screen #
MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => QueryCubit<List<User>>(
key: 'users',
queryFn: () => api.fetchUsers(),
),
),
BlocProvider(
create: (_) => QueryCubit<List<Post>>(
key: 'posts',
queryFn: () => api.fetchPosts(),
),
),
],
child: MyScreen(),
)
Security Features π #
fasq_bloc supports all FASQ security features through QueryClient configuration:
Secure Queries with QueryCubit #
BlocProvider(
create: (_) => QueryCubit<String>(
key: 'auth-token',
queryFn: () => api.getAuthToken(),
options: QueryOptions(
isSecure: true, // Mark as secure
maxAge: Duration(minutes: 15), // Required TTL
staleTime: Duration(minutes: 5),
),
client: context.queryClient, // Use configured client
),
child: BlocBuilder<QueryCubit<String>, QueryState<String>>(
builder: (context, state) {
// Secure data never persisted, cleared on app background
return Text('Token: ${state.data}');
},
),
)
Secure Mutations with MutationCubit #
BlocProvider(
create: (_) => MutationCubit<String, String>(
mutationFn: (data) => api.secureMutation(data),
options: MutationOptions(
queueWhenOffline: true,
maxRetries: 3,
),
client: context.queryClient, // Use configured client
),
child: BlocBuilder<MutationCubit<String, String>, MutationState<String>>(
builder: (context, state) {
return ElevatedButton(
onPressed: state.isLoading
? null
: () => context.read<MutationCubit<String, String>>().mutate('secure-data'),
child: state.isLoading
? CircularProgressIndicator()
: Text('Secure Mutation'),
);
},
),
)
Global Security Configuration #
QueryClientProvider(
config: CacheConfig(
defaultStaleTime: Duration(minutes: 5),
defaultCacheTime: Duration(minutes: 10),
),
persistenceOptions: PersistenceOptions(
enabled: true,
encryptionKey: 'your-encryption-key',
),
child: MaterialApp(
home: MyApp(),
),
)
Security Benefits:
- β Secure cache entries with automatic cleanup
- β Encrypted persistence for sensitive data
- β Input validation preventing injection attacks
- β Platform-specific secure key storage
Learn More #
- FASQ Documentation
- Bloc Documentation
- React Query (inspiration)
License #
MIT