Skip to content

MultiQueryBuilder Widget

Execute multiple queries in parallel with the Bloc adapter using a single widget.

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)),
],
);
},
);
}
}
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 configurations
  • builder: Function that receives the combined state and returns a widget
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 query
  • queryFn: Function that returns a Future with the data
  • options: Optional query configuration
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;
}
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();
},
)
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),
],
);
},
)

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

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
}

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
}
  • 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
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)),
],
);
},
);
}
}
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)),
],
);
},
);
}
}
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,
),
],
);
},
);
}
}
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 build
class 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),
);
}
}
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(),
);
},
);
}
}
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);
});
// Bad: Assumes index exists
MultiQueryBuilder(
configs: configs,
builder: (context, state) {
return Text(state.getState<String>(5).data ?? 'loading'); // Could crash
},
)
// Good: Check bounds
MultiQueryBuilder(
configs: configs,
builder: (context, state) {
if (state.length <= 5) return const SizedBox();
return Text(state.getState<String>(5).data ?? 'loading');
},
)
// Bad: No handling for empty configs
MultiQueryBuilder(
configs: [],
builder: (context, state) {
return DataWidget(state.getState(0)); // Will crash
},
)
// Good: Handle empty configs
MultiQueryBuilder(
configs: configs,
builder: (context, state) {
if (state.length == 0) return const SizedBox();
return DataWidget(state.getState(0));
},
)
// Bad: Manual state checking
MultiQueryBuilder(
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 helpers
MultiQueryBuilder(
builder: (context, state) {
if (state.isAllLoading) return CircularProgressIndicator();
if (state.hasAnyError) return ErrorWidget();
// ... use helpers
},
)