MultiQueryBuilder Widget
Execute multiple queries in parallel with the Bloc adapter using a single widget.
Basic Usage
Section titled “Basic Usage”import 'package:fasq_bloc/fasq_bloc.dart';
class Dashboard extends StatelessWidget { @override Widget build(BuildContext context) { return 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: [ UsersList(state.getState<List<User>>(0)), PostsList(state.getState<List<Post>>(1)), CommentsList(state.getState<List<Comment>>(2)), ], ); }, ); }}API Reference
Section titled “API Reference”MultiQueryBuilder
Section titled “MultiQueryBuilder”class MultiQueryBuilder extends StatefulWidget { final List<MultiQueryConfig> configs; final Widget Function(BuildContext context, MultiQueryState state) builder;
const MultiQueryBuilder({ super.key, required this.configs, required this.builder, });}Parameters:
configs: List of query configurationsbuilder: Function that receives the combined state and returns a widget
MultiQueryConfig
Section titled “MultiQueryConfig”class MultiQueryConfig { final String key; final Future<dynamic> Function() queryFn; final QueryOptions? options;
const MultiQueryConfig({ required this.key, required this.queryFn, this.options, });}Parameters:
key: Unique identifier for the queryqueryFn: Function that returns a Future with the dataoptions: Optional query configuration
MultiQueryState
Section titled “MultiQueryState”class MultiQueryState { final List<QueryState<dynamic>> states;
// Helper methods bool get isAllLoading; bool get isAnyLoading; bool get isAllSuccess; bool get hasAnyError; bool get isAllData;
QueryState<T> getState<T>(int index); int get length;}State Management
Section titled “State Management”Built-in Helper Methods
Section titled “Built-in Helper Methods”MultiQueryBuilder( builder: (context, state) { // Check if all queries are loading if (state.isAllLoading) { return const CircularProgressIndicator(); }
// Check if any query is loading if (state.isAnyLoading) { return const PartialLoadingWidget(); }
// Check if all queries succeeded if (state.isAllSuccess) { return const SuccessWidget(); }
// Check if any query has error if (state.hasAnyError) { return const ErrorWidget(); }
// Check if all queries have data if (state.isAllData) { return const DataWidget(); }
return const SizedBox(); },)Accessing Individual States
Section titled “Accessing Individual States”MultiQueryBuilder( configs: configs, builder: (context, state) { // Access by index with type safety final userState = state.getState<List<User>>(0); final postState = state.getState<List<Post>>(1); final commentState = state.getState<List<Comment>>(2);
return Column( children: [ UserList(userState), PostList(postState), CommentList(commentState), ], ); },)Named Queries with NamedMultiQueryBuilder
Section titled “Named Queries with NamedMultiQueryBuilder”For better developer experience, you can use named queries with map-based access:
import 'package:fasq_bloc/fasq_bloc.dart';
class Dashboard extends StatelessWidget { @override Widget build(BuildContext context) { return 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')), ], ); }, ); }}NamedQueryConfig
Section titled “NamedQueryConfig”The NamedQueryConfig class provides configuration for named queries:
class NamedQueryConfig { final String name; // Name identifier for this query final String key; // Unique identifier for this query final Future<dynamic> Function() queryFn; // Function that returns a Future with the data final QueryOptions? options; // Optional configuration for this query}NamedQueryState
Section titled “NamedQueryState”The NamedQueryState class provides helper methods for named queries:
class NamedQueryState { // Aggregate state helpers bool get isAllLoading; // True if all queries are loading bool get isAnyLoading; // True if any query is loading bool get isAllSuccess; // True if all queries succeeded bool get hasAnyError; // True if any query has error bool get isAllData; // True if all queries have data
// Named access methods QueryState<T> getState<T>(String name); // Get state by name bool isLoading(String name); // Check if specific query is loading bool hasError(String name); // Check if specific query has error int get length; // Number of queries}Benefits of Named Access
Section titled “Benefits of Named Access”- Better DX: Access queries by meaningful names instead of indices
- Type Safety: Compile-time checking for query names
- Self-Documenting: Code is more readable and maintainable
- Refactoring Safe: Renaming queries updates all references
Advanced Patterns
Section titled “Advanced Patterns”Conditional Queries
Section titled “Conditional Queries”class ConditionalDashboard extends StatelessWidget { final bool loadComments;
const ConditionalDashboard({required this.loadComments});
@override Widget build(BuildContext context) { final configs = [ MultiQueryConfig(key: 'users', queryFn: () => api.fetchUsers()), MultiQueryConfig(key: 'posts', queryFn: () => api.fetchPosts()), if (loadComments) MultiQueryConfig(key: 'comments', queryFn: () => api.fetchComments()), ];
return MultiQueryBuilder( configs: configs, builder: (context, state) { return Column( children: [ UserList(state.getState<List<User>>(0)), PostList(state.getState<List<Post>>(1)), if (loadComments) CommentList(state.getState<List<Comment>>(2)), ], ); }, ); }}Error Handling
Section titled “Error Handling”class ErrorHandlingDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return 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: [ // Show error banner if any query failed if (state.hasAnyError) ErrorBanner( onRetry: () { // Retry all failed queries for (int i = 0; i < state.length; i++) { final queryState = state.getState(i); if (queryState.hasError) { queryState.refetch(); } } }, ),
// Show loading indicator if any query is loading if (state.isAnyLoading) const LinearProgressIndicator(),
// Show data for successful queries if (state.getState<List<User>>(0).hasData) UserList(state.getState<List<User>>(0)), if (state.getState<List<Post>>(1).hasData) PostList(state.getState<List<Post>>(1)), if (state.getState<List<Comment>>(2).hasData) CommentList(state.getState<List<Comment>>(2)), ], ); }, ); }}Partial Loading States
Section titled “Partial Loading States”class PartialLoadingDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return 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: [ // Show individual loading states UserSection( state: state.getState<List<User>>(0), isLoading: state.getState<List<User>>(0).isLoading, ), PostSection( state: state.getState<List<Post>>(1), isLoading: state.getState<List<Post>>(1).isLoading, ), CommentSection( state: state.getState<List<Comment>>(2), isLoading: state.getState<List<Comment>>(2).isLoading, ), ], ); }, ); }}Performance Tips
Section titled “Performance Tips”Stable Config Lists
Section titled “Stable Config Lists”class Dashboard extends StatelessWidget { // Good: Static config list static const _configs = [ MultiQueryConfig(key: 'users', queryFn: () => api.fetchUsers()), MultiQueryConfig(key: 'posts', queryFn: () => api.fetchPosts()), ];
@override Widget build(BuildContext context) { return MultiQueryBuilder( configs: _configs, builder: (context, state) => DataWidget(state), ); }}
// Avoid: Creating new list on each buildclass Dashboard extends StatelessWidget { @override Widget build(BuildContext context) { return MultiQueryBuilder( configs: [ MultiQueryConfig(key: 'users', queryFn: () => api.fetchUsers()), MultiQueryConfig(key: 'posts', queryFn: () => api.fetchPosts()), ], builder: (context, state) => DataWidget(state), ); }}Memoized Configs
Section titled “Memoized Configs”class DynamicDashboard extends StatefulWidget { final List<String> userIds;
const DynamicDashboard({required this.userIds});
@override State<DynamicDashboard> createState() => _DynamicDashboardState();}
class _DynamicDashboardState extends State<DynamicDashboard> { late final List<MultiQueryConfig> _configs;
@override void initState() { super.initState(); _configs = widget.userIds.map((id) => MultiQueryConfig(key: 'user-$id', queryFn: () => api.fetchUser(id)) ).toList(); }
@override Widget build(BuildContext context) { return MultiQueryBuilder( configs: _configs, builder: (context, state) { return Column( children: widget.userIds.asMap().entries.map((entry) { final index = entry.key; final userId = entry.value; final userState = state.getState<User>(index);
return UserCard( userId: userId, state: userState, ); }).toList(), ); }, ); }}Testing
Section titled “Testing”testWidgets('MultiQueryBuilder executes all queries', (tester) async { await tester.pumpWidget( MaterialApp( home: MultiQueryBuilder( configs: [ MultiQueryConfig(key: 'query1', queryFn: () => Future.value('data1')), MultiQueryConfig(key: 'query2', queryFn: () => Future.value('data2')), ], builder: (context, state) { return Column( children: [ Text(state.getState<String>(0).data ?? 'loading'), Text(state.getState<String>(1).data ?? 'loading'), ], ); }, ), ), );
await tester.pumpAndSettle();
expect(find.text('data1'), findsOneWidget); expect(find.text('data2'), findsOneWidget);});Common Pitfalls
Section titled “Common Pitfalls”1. Index Out of Bounds
Section titled “1. Index Out of Bounds”// Bad: Assumes index existsMultiQueryBuilder( configs: configs, builder: (context, state) { return Text(state.getState<String>(5).data ?? 'loading'); // Could crash },)
// Good: Check boundsMultiQueryBuilder( configs: configs, builder: (context, state) { if (state.length <= 5) return const SizedBox(); return Text(state.getState<String>(5).data ?? 'loading'); },)2. Not Handling Empty Configs
Section titled “2. Not Handling Empty Configs”// Bad: No handling for empty configsMultiQueryBuilder( configs: [], builder: (context, state) { return DataWidget(state.getState(0)); // Will crash },)
// Good: Handle empty configsMultiQueryBuilder( configs: configs, builder: (context, state) { if (state.length == 0) return const SizedBox(); return DataWidget(state.getState(0)); },)3. Ignoring State Helpers
Section titled “3. Ignoring State Helpers”// Bad: Manual state checkingMultiQueryBuilder( builder: (context, state) { final allLoading = state.states.every((s) => s.isLoading); final anyError = state.states.any((s) => s.hasError); // ... manual checks },)
// Good: Use built-in helpersMultiQueryBuilder( builder: (context, state) { if (state.isAllLoading) return CircularProgressIndicator(); if (state.hasAnyError) return ErrorWidget(); // ... use helpers },)