Skip to content

QueryBuilder

The QueryBuilder widget is the heart of fetching data in Fasq. It turns asynchronous data sources into reactive UI states.

import 'package:fasq/fasq.dart';
QueryBuilder<List<Todo>>(
queryKey: 'todos'.toQueryKey(),
queryFn: () => fetchTodos(),
builder: (context, state) {
// 1. Loading State
if (state.isLoading) {
return const CircularProgressIndicator();
}
// 2. Error State
if (state.hasError) {
return Text('Error: ${state.error}');
}
// 3. Success State
return ListView(
children: state.data!.map((todo) => Text(todo.title)).toList(),
);
},
)
NameTypeRequiredDescription
queryKeyQueryKeyYesUnique identifier for caching and sharing the query. Use 'key'.toQueryKey().
queryFnFuture<T> Function()NoThe function that fetches the data. Required if queryFnWithToken is not provided.
queryFnWithTokenFuture<T> Function(CancellationToken)NoFunction that fetches data with a cancellation token. Required if queryFn is not provided.
builderWidget Function(BuildContext, QueryState<T>)YesBuilds the UI based on the current state.
optionsQueryOptions?NoConfiguration for caching, refetching, and side effects.
dependsOnQueryKey?NoKey of a parent query. If the parent is disposed, this query is cancelled.

The state object passed to the builder contains:

PropertyTypeDescription
dataT?The data returned from the query function. null if not loaded or errored.
errorObject?The error object if the query failed.
statusQueryStatusCurrent status: loading, success, or error.
isLoadingboolTrue if the query is performing the initial fetch (hard loading).
isFetchingboolTrue if the query is fetching, including background refetches.
hasDataboolTrue if data is not null.
hasErrorboolTrue if error is not null.

You can pass QueryOptions to customize how the query behaves.

QueryBuilder<User>(
queryKey: 'user:1'.toQueryKey(),
queryFn: () => fetchUser(1),
options: const QueryOptions(
staleTime: Duration(minutes: 5), // Data remains fresh for 5 mins
refetchOnResume: true, // Refetch when app comes to foreground
),
builder: (context, state) {
if (state.hasData) return UserProfile(state.data!);
return const LoadingSpinner();
},
)

Distinguish between initial loading and background updates.

QueryBuilder<List<Post>>(
queryKey: 'feed'.toQueryKey(),
queryFn: () => fetchFeed(),
builder: (context, state) {
if (state.isLoading) return const ShimmerList();
return Column(
children: [
// Show a small loader when refreshing in background
if (state.isFetching) const LinearProgressIndicator(),
Expanded(
child: ListView.builder(
itemCount: state.data?.length ?? 0,
itemBuilder: (_, i) => PostTile(state.data![i]),
),
),
],
);
},
)

Queries can depend on variables. FASQ handles key changes automatically.

class UserPosts extends StatelessWidget {
final String userId;
const UserPosts({required this.userId});
@override
Widget build(BuildContext context) {
return QueryBuilder<List<Post>>(
// The key includes the userId. If userId changes,
// Fasq automatically fetches data for the new user.
queryKey: 'posts:$userId'.toQueryKey(),
queryFn: () => fetchPostsForUser(userId),
builder: (context, state) {
// ... build UI
},
);
}
}

Fasq supports cancelling in-flight requests when a query is disposed or re-fetched. use queryFnWithToken to access the cancellation token.

/// Example using Dio
QueryBuilder<List<Todo>>(
queryKey: 'todos'.toQueryKey(),
// Use queryFnWithToken instead of queryFn
queryFnWithToken: (token) async {
final cancelToken = CancelToken();
// Register cancellation callback
token.onCancel(() => cancelToken.cancel());
try {
final response = await dio.get(
'/todos',
cancelToken: cancelToken,
);
return (response.data as List).map((e) => Todo.fromJson(e)).toList();
} on DioException catch (e) {
// Fasq handles cancellations silently if they throw exceptions
// matching your client's cancellation error type.
// If you rethrow properly, Fasq will recognize it.
rethrow;
}
},
builder: (context, state) {
if (state.isLoading) return const CircularProgressIndicator();
return TodoList(state.data!);
},
)
/// Example using http package
QueryBuilder<String>(
queryKey: 'data'.toQueryKey(),
queryFnWithToken: (token) async {
final client = http.Client();
// Close client on cancellation
token.onCancel(() => client.close());
try {
final response = await client.get(Uri.parse('https://example.com'));
return response.body;
} finally {
// Clean up if not cancelled via token
if (!token.isCancelled) {
client.close();
}
}
},
builder: (context, state) => Text(state.data ?? 'Loading...'),
)