fquery

repository·dev·Indexed 41 days ago

https://github.com/41y08h/fquery

A 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.

Tokens
11.5K
Snippets
34
Records
43
Agent score
50%

What's inside fquery

  1. Overview of fquery

    dev
    fquery is an asynchronous state management solution for Flutter designed to manage server state (REST API, GraphQL, etc.), local databases (SQLite), or any other asynchronous data. It provides automatic caching, garbage collection, and re-fetching of stale data, making async state management as simple as managing synchronous variables. It is data-fetching agnostic, meaning it works with any Future provided to it.
  2. Implement Dependent Queries

    dev

    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');
          }
        )
      }
    )
  3. Access queries outside of builders using Instance classes

    dev

    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();
      }
    }
  4. Use fquery with flutter_hooks

    dev

    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
    useIsFetching
  5. Setup fquery with CacheProvider

    dev

    To 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(
  6. Configure default QueryCache settings

    dev

    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(
    // ...
  7. Manage query state with QueryCache

    dev

    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/Get: Use build to retrieve a query or create it if it doesn't exist.
    • Manual Updates: Use setQueryData to manually update the data for a specific key.
    • Invalidation: Use invalidateQueries to mark queries as stale, triggering refetches.
    • Removal: Use removeQueries to purge specific queries from the cache.
    • Observability: It implements 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']));
  8. Use QueryBuilder to manage asynchronous data

    dev

    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),
                );
              },
            );
          },
        );
      }
    }
  9. Run multiple queries with QueriesBuilder

    dev

    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) {
        // ...
      },
    )
  10. Invalidate queries manually

    dev

    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).

    • Partial matching: cache.invalidateQueries(['posts']) invalidates all keys starting with ['posts'] (e.g., ['posts', 1]).
    • Exact matching: 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);
  11. Use QueryBuilder for data fetching

    dev

    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).

    QueryOptions Reference

    ParameterDescription
    enabledIf false, the fetcher function is not automatically called. Useful for dependent queries.
    cacheDurationDuration unused/inactive cache data remains in memory before garbage collection.
    refetchIntervalTime interval for automatic re-fetching (polling). Set to null to disable.
    refetchOnMountBehavior when the widget is first built. Options: RefetchOnMount.always, RefetchOnMount.stale, RefetchOnMount.never.
    staleDurationDuration until the cached data is considered stale.
    retryCountNumber of retries before showing an error.
    retryDelayDelay between retries.
    QueryBuilder(
      options: QueryOptions(
        queryKey: QueryKey(['users', email]),
        queryFn: getUserByEmail,
      ),
      builder: (context, user) {
        // ...
      },
    )
  12. Manually update query data with setQueryData

    dev

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