flutter_query_client 1.0.1 copy "flutter_query_client: ^1.0.1" to clipboard
flutter_query_client: ^1.0.1 copied to clipboard

A TanStack Query-inspired server state management library for Flutter. Handles caching, background refetching, pagination, mutations, and network-aware fetching out of the box.

flutter_query_client #

A TanStack Query-inspired server state management library for Flutter.

Handles fetching, caching, synchronizing, and updating server state with minimal boilerplate — automatic background refetching, stale-while-revalidate caching, infinite pagination, and mutation support out of the box.


Features #

  • QueryController — fetch and cache server data automatically
  • MutationController — trigger user-driven mutations (create, update, delete)
  • InfiniteQueryController — paginated / infinite-scroll data with loadMore()
  • Stale-while-revalidate — serve cached data instantly, refetch in background
  • Configurable stale time & garbage collection — control cache lifetime
  • Automatic retry with exponential backoff — resilient to transient failures
  • Network-aware fetching — pauses when offline, resumes on reconnect
  • Refetch on mount — always, only if stale, or never
  • Keep previous data - keeps previous data when fetching data
  • Global defaults with per-controller overrides
  • BLoC-based state — integrates naturally with flutter_bloc
  • Freezed immutable state — safe pattern matching on QueryState

Installation #

dependencies:
  flutter_query_client: ^1.0.0

Quick Start #

1. Wrap your app with QueryClientProvider #

void main() {
  runApp(
    QueryClientProvider(
      defaults: QueryDefaults(
        staleTime: const Duration(minutes: 5),
        gcTime: const Duration(minutes: 10),
        retryCount: 3,
      ),
      child: const MyApp(),
    ),
  );
}

2. Define a QueryController #

class PostsController extends QueryController<List<Post>, void> {
  PostsController() : super(baseKey: 'posts');

  @override
  Future<List<Post>> queryFn(void params) async {
    return postService.getPosts();
  }
}

3. Provide and consume it in your widget tree #

QueryProvider<PostsController, List<Post>>(
  create: (_) => PostsController(),
  builder: (context, controller) {
    return QueryBuilder<PostsController, List<Post>>(
      controller: controller,
      builder: (context, state) {
        if (state.isLoading) return const CircularProgressIndicator();
        if (state.isError) return Text('Error: ${state.error}');
        return ListView(
          children: state.data!.map((p) => Text(p.title)).toList(),
        );
      },
    );
  },
);

Core Concepts #

QueryState #

Every controller exposes a QueryState<T>:

Property Description
data The cached data (nullable)
error The error (nullable)
status idle / loading / success / error
fetchStatus idle / fetching / refetching / paused
isStale Whether data is past its stale time
isLoading status == loading
isSuccess status == success
isError status == error
isFetching Actively fetching (initial or background)
isRefetching Background refetch with existing data
isPaused Paused due to no network
hasData data != null
hasError error != null

QueryController #

Use for any single-resource fetch (list, detail, etc.).

class PostByIdController extends QueryController<Post, int> {
  PostByIdController() : super(baseKey: 'posts');

  @override
  Future<Post> queryFn(int id) => postService.getPostById(id);
}

// Set params to trigger the fetch
controller.setParams(postId);

// Manually refetch
controller.refetch();

// Update cache directly (optimistic update)
controller.updateCache((current) => current.copyWith(title: 'New title'));

// Invalidate (marks stale + refetches if active)
controller.invalidate();

Constructor options #

QueryController({
  required String baseKey,
  Duration? staleTime,           // Default: from QueryDefaults
  Duration? gcTime,              // Default: from QueryDefaults
  int? retryCount,               // Default: 3
  Duration? retryDelay,          // Default: exponential backoff
  Duration? refetchInterval,     // Poll interval (null = disabled)
  RefetchOnMount refetchOnMount, // always | stale | never
  NetworkMode networkMode,       // online | always | offlineFirst
  RefetchOnReconnect refetchOnReconnect, // always | ifStale | never
})

MutationController #

Use for create / update / delete operations. Mutations do not auto-retry by default.

class CreatePostMutation extends MutationController<Post> {
  CreatePostMutation(this._client) : super(queryClient: _client);
  final QueryClient _client;

  @override
  Future<Post> mutationFn(dynamic params) async {
    final post = await postService.createPost(params as Map<String, dynamic>);
    // Invalidate the list to trigger a refetch
    _client.invalidate('posts');
    return post;
  }

  @override
  void onSuccess(Post data) {
    // Optimistically update the cache
    _client.update<List<Post>>('posts', null, (posts) => [data, ...posts!]);
  }
}
// Trigger the mutation
mutation.mutate({'title': 'Hello', 'body': 'World'});

// React to result with QueryListener
QueryListener<CreatePostMutation, Post>(
  controller: mutation,
  onSuccess: (post) => Navigator.pop(context),
  onError: (err) => showSnackBar(err.toString()),
  child: ...,
);

InfiniteQueryController #

Use for paginated or infinite-scroll data.

class ProductsController extends InfiniteQueryController<Product, int, String> {
  ProductsController() : super(baseKey: 'products', initialPageParam: 1);

  @override
  Future<List<Product>> queryFn(int page, String query) =>
      productService.getProducts(page: page, search: query);

  @override
  int? getNextPageParam(List<Product> lastPage, int currentPage) =>
      lastPage.length == 20 ? currentPage + 1 : null;
}
InfiniteQueryProvider<Product>(
  create: (_) => ProductsController(),
  builder: (context, controller) {
    return InfiniteQueryBuilder<ProductsController, Product, int, String>(
      controller: controller,
      builder: (context, state) {
        return ListView.builder(
          itemCount: state.data?.length ?? 0,
          itemBuilder: (_, i) => ProductTile(state.data![i]),
        );
      },
    );
  },
);

// Trigger next page
controller.loadMore();

// Check if more pages exist
controller.hasMore;

QueryClient #

The QueryClient is a singleton that manages the cache. Access it via context:

final client = QueryClientProvider.of(context).client;

// Invalidate a specific key (marks stale + refetches active controllers)
client.invalidate('posts');

// Invalidate all queries matching a prefix
client.invalidateQueries('posts');

// Update cached data directly
client.update<List<Post>>('posts', null, (posts) => [...posts!, newPost]);

// Read cached data
final cached = client.get<List<Post>>('posts', null);

Global Configuration #

Configure defaults once in QueryClientProvider:

QueryClientProvider(
  defaults: QueryDefaults(
    staleTime: const Duration(minutes: 5),
    gcTime: const Duration(minutes: 10),
    retryCount: 3,
    retryDelay: const Duration(seconds: 2),
    networkMode: NetworkMode.online,
    refetchOnReconnect: RefetchOnReconnect.ifStale,
    refetchOnMount: RefetchOnMount.stale,
    errorTransform: (error) => AppException.from(error),
    connectivityEndpoints: [
      InternetCheckOption(uri: Uri.parse('https://your-api.com/health')),
    ],
    enableLogging: true,
  ),
  child: const MyApp(),
),

Network Modes #

Mode Behaviour
NetworkMode.online Pauses fetching when offline, resumes on reconnect
NetworkMode.always Fetches regardless of network status
NetworkMode.offlineFirst Fetches immediately, then pauses on error if offline

Widgets Reference #

Widget Purpose
QueryProvider<C, T> Provides a QueryController via BlocProvider
InfiniteQueryProvider<T> Provides an InfiniteQueryController
QueryBuilder<C, T> Rebuilds on every state change
InfiniteQueryBuilder<C, T, P, Param> Builder for infinite queries
QueryListener<C, T> Side effects without rebuilding
MultiQueryListener Listen to multiple controllers at once

Example #

A full example app demonstrating CRUD for Posts and infinite-scroll Products is available in the example/ directory.


2
likes
0
points
228
downloads

Publisher

unverified uploader

Weekly Downloads

A TanStack Query-inspired server state management library for Flutter. Handles caching, background refetching, pagination, mutations, and network-aware fetching out of the box.

Repository (GitHub)
View/report issues

Topics

#state-management #caching #fquery #flutter-query #tanstack-query

License

unknown (license)

Dependencies

bloc, connectivity_plus, flutter, flutter_bloc, freezed_annotation, internet_connection_checker_plus, logging, meta

More

Packages that depend on flutter_query_client