Overview of fquery
devFuture provided to it.repository·dev·Indexed 41 days ago
https://github.com/41y08h/fqueryA Dart library for Flutter that simplifies asynchronous state management for server state (REST API, GraphQL) and local databases. It provides automatic caching, garbage collection, and re-fetching of stale data via QueryBuilder, InfiniteQueryBuilder, and MutationBuilder. The library is data-fetching agnostic and includes integration hooks for flutter_hooks.
Future provided to it.A dependent query is a query that only executes when certain conditions are met (e.g., after another query finishes or a variable changes). Use the enabled property in QueryOptions to control this.
In the example below, the second query (fetching posts) only starts once the first query (fetching user) provides a valid username.
QueryBuilder(
options: QueryOptions(
queryKey: QueryKey(['users', email]),
queryFn: getUserByEmail,
),
builder: (context, user) {
return QueryBuilder(
options: QueryOptions(
enabled: user.data?.username != null,
queryKey: QueryKey(['posts', email]),
queryFn: () {
return getPostsByUsername(user.data!.username);
},
),
builder: (context, posts) {
if(posts.isFetching) {
return Text('Waiting to start fetch');
}
if(posts.isLoading) {
return Text('Loading...');
}
return Text('foo bar');
}
)
}
)If you need to access a query instance in a part of your code where you have access to BuildContext but are not inside a builder (e.g., in a scroll listener or a callback), you can use the static .of() method provided by the instance classes.
Warning: Accessing results this way via Instance.of will not trigger widget rebuilds. Use this pattern only for performing actions (like calling fetchNextPage()) rather than for rendering data that depends on the query state.
void onScroll() {
// Access the infinite query instance using the context and its options
final itemsQuery = InfiniteQueryInstance.of(
context,
itemsQueryOptions,
);
// Perform an action on the query
if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
itemsQuery.fetchNextPage();
}
}If you are using flutter_hooks, fquery provides a set of ready-to-use hooks to manage query states seamlessly within your widget lifecycle. These hooks allow you to access query data, loading states, and mutation controls directly within a HookWidget.
// Available hooks:
useQuery
useInfiniteQuery
useQueries
useMutation
useIsFetchingTo use fquery, you must wrap your application or the relevant widget tree with a CacheProvider. This provides the QueryCache instance to your widget tree.
void main() {
runApp(
CacheProvider(
cache: queryCache,
child: CupertinoApp(
// ...
),
),
);
}void main() {
runApp(
CacheProvider(
cache: queryCache,
child: CupertinoApp(You can define global default behaviors for all queries by configuring the QueryCache with DefaultQueryOptions. This is passed to the CacheProvider during app initialization.
final cache = QueryCache(
defaultQueryOptions: DefaultQueryOptions(
cacheDuration: Duration(minutes: 20),
refetchInterval: Duration(minutes: 5),
refetchOnMount: RefetchOnMount.always,
staleDuration: Duration(minutes: 3),
),
);
void main() {
runApp(
CacheProvider(
cache: cache,
child: CupertinoApp(
// ...The QueryCache is the central state container for all queries in fquery. It manages the lifecycle of queries, including building new queries, reading existing ones, updating data, invalidating stale entries, and garbage-collecting unused queries based on their cacheDuration.
Key capabilities:
build to retrieve a query or create it if it doesn't exist.setQueryData to manually update the data for a specific key.invalidateQueries to mark queries as stale, triggering refetches.removeQueries to purge specific queries from the cache.Observable<QueryKey>, allowing you to listen to changes in the cache.final queryCache = QueryCache(defaultQueryOptions: DefaultQueryOptions(...));
// Build or retrieve a query
final query = queryCache.build<MyDataType>(queryKey: QueryKey(['my-key']));The QueryBuilder widget is the primary way to consume asynchronous data in your UI. You provide QueryOptions containing a queryKey (used for caching) and a queryFn (the function that returns a Future). The builder function provides access to the current state of the query, including isLoading, isError, error, and data.
class TodoWidget extends StatelessWidget {
const TodoWidget({super.key});
@override
Widget build(BuildContext context) {
return QueryBuilder<List<Todo>, Exception>(
options: QueryOptions(
queryKey: QueryKey(['todos']),
queryFn: TodosAPI.getInstance().getAll,
),
builder: (context, todos) {
if (todos.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (todos.isError) {
return Center(child: Text(todos.error!.toString()));
}
return ListView.builder(
itemCount: todos.data!.length,
itemBuilder: (context, index) {
final todo = todos.data![index];
return ListTile(
title: Text(todo.text),
);
},
);
},
);
}
}While Column or nesting can be used for a fixed number of parallel queries, QueriesBuilder is used for dynamic parallel queries where the number of queries is determined at runtime (e.g., rendering a list of items where each item needs its own query).
// no. of queries are changing according to `text`
QueriesBuilder<Post, Exception>(
options: List<QueryOptions<Post, Exception>>.generate(
text,
(i) => QueryOptions(
queryKey: QueryKey(['posts', i + 1]),
queryFn: () => getPost(i + 1),
refetchOnMount: RefetchOnMount.never,
),
),
builder: (context, results) {
// ...
},
)You can manually mark cached data as stale or trigger a re-fetch using invalidateQueries() on the QueryCache. This is useful when you know data has changed on the server.
To access the cache, use CacheProvider.of(context) or CacheProvider.get(context).
cache.invalidateQueries(['posts']) invalidates all keys starting with ['posts'] (e.g., ['posts', 1]).cache.invalidateQueries(['posts'], exact: true) only invalidates the exact key ['posts'].final cache = CacheProvider.of(context);
// Invalidate every query with a key that starts with `post'
cache.invalidateQueries(['posts']);
// Use `exact: true` to exactly match the query
cache.invalidateQueries(['posts'], exact: true);The QueryBuilder widget is used to fetch and display asynchronous data. You must provide a queryKey (an identifier for the data in the QueryCache) and a queryFn (the async function that fetches the data).
| Parameter | Description |
|---|---|
enabled | If false, the fetcher function is not automatically called. Useful for dependent queries. |
cacheDuration | Duration unused/inactive cache data remains in memory before garbage collection. |
refetchInterval | Time interval for automatic re-fetching (polling). Set to null to disable. |
refetchOnMount | Behavior when the widget is first built. Options: RefetchOnMount.always, RefetchOnMount.stale, RefetchOnMount.never. |
staleDuration | Duration until the cached data is considered stale. |
retryCount | Number of retries before showing an error. |
retryDelay | Delay between retries. |
QueryBuilder(
options: QueryOptions(
queryKey: QueryKey(['users', email]),
queryFn: getUserByEmail,
),
builder: (context, user) {
// ...
},
)If you want to update the cache without a full re-fetch (e.g., after a local mutation), use setQueryData(). It takes a queryKey and an updater function. The updater function receives the previous data (which is nullable if the key doesn't exist).
Note: The returned type must match the type of data stored in the cache, otherwise an error is thrown.
final cache = CacheProvider.get(context);
cache.setQueryData<List<Post>>(['posts'], (previous) {
return previous?.map((post) {
return post.copyWith(
title: "lorem ipsum"
);
}).toList() ?? <Post>[];
});