Skip to main content

Server data in Flutter, cached and kept fresh

A port of TanStack Query to Dart and Flutter. You describe where the data comes from; the cache decides when to fetch, share, refresh and forget it.

flutter pub add query_kit_flutter
task_list.dart
QueryObserverOptions<List<Task>> tasksQuery() => QueryObserverOptions(
queryKey: QueryKey(<Object?>['tasks']),
queryFn: (context) => api.listTasks(signal: context.signal),
);

class TaskList extends StatelessWidget {
const TaskList({super.key});


Widget build(BuildContext context) {
final tasks = context.query(tasksQuery());

return switch (tasks) {
QueryPending() => const CircularProgressIndicator(),
QueryError(:final error) => Text('Could not load: $error'),
QuerySuccess(:final data) => ListView(
children: <Widget>[
for (final task in data) Text(task.name),
],
),
};
}
}

Server state is not app state.

A selected tab or a half-typed form is yours: it stays where you left it. The orders on a server are not. Someone else can change them, your copy ages while the user reads it, three screens want it at once, and the network drops at the worst moment.

A FutureBuilder gets the first load on screen. What comes after is the real work: caching, sharing one request between readers, refreshing when the app comes back, retrying, cancelling, invalidating after a write, paging. query_kit does that work the way TanStack Query does it on the web, so a screen only says what it shows.

The longer version

A write on screen before the server answers.

The showcase's optimistic-updates screen, running in this page against an in-memory backend. Add a todo and it is in the list at once. Then switch on Refuse next write, choose Via cache and add another: the row appears, the server refuses it, and the cache rolls back.

Live demoOptimistic updatesShow the write before the server answers — two ways.~3 MB, runs in your browser; no server involved.

The work after the first load, done by the cache.

The cache

  • QueryKey
  • StaleTime
  • GcTime
Caching and deduplication
Five widgets reading one key make one request and share its answer.
Stale-while-revalidate
A second visit renders from the cache at once, and refetches behind it when the data is older than its staleTime.
Refetch on focus and reconnect
The app returning to the foreground, or the network returning, refreshes what is on screen.
Infinite and paginated lists
Pages fetched in either direction, kept under one key, refetched in order.

Writes

  • MutationOptions
  • onMutate
  • invalidateQueries
Mutations and invalidation
A write names the keys it made stale, and whatever shows them fetches again.
Optimistic updates
Show the write before the server answers, and roll it back when the server refuses.

The network

  • RetryPolicy
  • context.signal
  • NetworkMode
Retries and cancellation
Failed fetches retry with backoff; a fetch whose function reads context.signal is cancelled when its last reader leaves.
Offline-aware
Told when the device is offline, queries wait for the network instead of failing, and writes made offline pause until it is back.

In your code

  • context.query
  • QueryBuilder
  • QueryMixin
Four equal ways to read
context.query, QueryBuilder, QueryMixin or a ValueListenable. None of them is the default.
Sealed results
A switch over QueryPending, QueryError and QuerySuccess is exhaustive, so there is no data!.
A pure-Dart core
The cache runs without Flutter: in a CLI, on a server, in a shared package.
Nothing but Flutter
No hooks, signals or connectivity package required. Connectivity is a Stream<bool> you bring yourself.

Upstream's own tests say it behaves the same.

TanStack Query's test suite is ported alongside the code, under upstream's own test names, so the two files read side by side. Every case that is not ported is listed with its reason, and where the Dart port differs on purpose, the differences are written down as behaviour.

Three of the ported cases, with the name they carry in both suites
Test nameUpstreamquery_kit
should go through a pending state even when the queryFn returns synchronouslyqueryObserver.test.tsxquery_observer_test.dart
should continue retry after reconnect and resolve all promisesquery.test.tsxquery_test.dart
should apply the maxPages option to limit the number of pagesinfiniteQueryBehavior.test.tsxinfinite_query_behavior_test.dart

Credits

Thank you to Tanner Linsley and to everyone who has built and maintained TanStack Query. Every good idea here is theirs, published under their MIT licence, whose notice each package carries in LICENSE-TANSTACK.

It is not theirs. Not affiliated with, endorsed by, reviewed by, or connected in any way to Tanner Linsley, the TanStack team, or the TanStack organisation. Please do not take problems with this package to them; they belong here.

Two packages on pub.dev.

query_kit_flutter

For a Flutter app: the provider, controllers, builder widgets, a State mixin and context.query, with the core re-exported.

flutter pub add query_kit_flutter

Flutter 3.27 or later. MIT licence.

query_kit

The pure-Dart core, for a CLI, a server, or a package shared with one.

dart pub add query_kit

Dart 3.6 or later. MIT licence.