Skip to main content

One-file tour

The binding ships one example with the package, and it is the one pub.dev shows on its Example tab: a single main.dart of some 130 lines that you can paste into a fresh flutter create project and run. It has no server; the "API" is a list behind a delay. It is the whole loop of a screen that reads and writes remote data, small enough to hold in your head: a provider at the root, a list read in build, a refresh indicator in the app bar that reads the same cache entry through a builder, and an add button whose mutation invalidates the list when it succeeds.

There is no live demo of this one; the showcase's Simple and Mutations screens cover the same ground with more to look at.

Walking through it​

The stand-in for an HTTP client, and the query built on it: one key, one function, a stale time of 30 seconds.

packages/query_kit_flutter/example/lib/main.dart · lines 14–37
class Api {
final List<String> _tasks = ['Kitchen', 'Hallway', 'Garage'];

Future<List<String>> list() async {
await Future<void>.delayed(const Duration(milliseconds: 600));
return List.unmodifiable(_tasks);
}

Future<void> add(String name) async {
await Future<void>.delayed(const Duration(milliseconds: 400));
_tasks.add(name);
}
}

final api = Api();
final tasksKey = QueryKey(<Object?>['tasks']);

Future<List<String>> fetchTasks(QueryFunctionContext context) => api.list();

QueryObserverOptions<List<String>> tasksQuery() => QueryObserverOptions(
queryKey: tasksKey,
queryFn: fetchTasks,
staleTime: const StaleTime.duration(Duration(seconds: 30)),
);

A second options object for the same key, reduced by select. Its reader shares the cache entry. It still rebuilds whenever the result changes (a refetch changes isFetching), which is exactly what a spinner wants.

packages/query_kit_flutter/example/lib/main.dart · lines 44–49
QuerySelectOptions<List<String>, bool> fetchingQuery() => QuerySelectOptions(
queryKey: tasksKey,
queryFn: fetchTasks,
staleTime: const StaleTime.duration(Duration(seconds: 30)),
select: (_) => true,
);

The client goes at the root, above MaterialApp, so every route can reach it.

packages/query_kit_flutter/example/lib/main.dart · lines 51–58
void main() {
runApp(
QueryClientProvider(
client: QueryClient(),
child: const MaterialApp(home: TasksScreen()),
),
);
}

The screen reads the list with context.query, takes the client in build (a mutation's callback can run after the widget that started it is gone, so it closes over the client rather than the BuildContext), and declares the add mutation with context.mutation. The app bar reads the select options through a QuerySelectBuilder; the body switches over the list's sealed result. These are two of the binding's four equal call styles, side by side on one key.

packages/query_kit_flutter/example/lib/main.dart · lines 60–129
class TasksScreen extends StatelessWidget {
const TasksScreen({super.key});


Widget build(BuildContext context) {
// One of the four equal call styles: read in build. The widget rebuilds
// when the result changes.
final tasks = context.query(tasksQuery());
// The client, taken here in `build` rather than inside the callback
// below. A mutation outlives the widget that started it — disposing its
// controller does not cancel it — so `onSuccess` can run after this
// element is gone, and looking an ancestor up from a deactivated element
// throws. The cache work has to happen either way; the client is the
// right thing to close over, the `BuildContext` is not.
final client = QueryClientProvider.of(context);
// A mutation, the same way. `MutationOptions.simple` is the form without
// an `onMutate` step: its types come from `api.add`.
final add = context.mutation(
MutationOptions.simple(
mutationFn: api.add,
onSuccess: (_, __, ___) => client.invalidateQueries(
filters: QueryFilters(queryKey: tasksKey),
),
),
);

return Scaffold(
appBar: AppBar(
title: const Text('Tasks'),
actions: <Widget>[
// Another, equally valid: a builder, for a leaf that only wants one
// flag.
QuerySelectBuilder<List<String>, bool>(
options: fetchingQuery(),
builder: (context, result) => result.isFetching
? const Padding(
padding: EdgeInsets.all(16),
child: SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => result.refetch(),
),
),
],
),
body: switch (tasks) {
QueryPending() => const Center(child: CircularProgressIndicator()),
QueryError(:final error, staleData: null) =>
Center(child: Text('$error')),
QuerySuccess(:final data) ||
QueryError(staleData: final data!) =>
ListView(
children: <Widget>[
for (final name in data) ListTile(title: Text(name)),
],
),
},
floatingActionButton: FloatingActionButton(
onPressed: add.value.isPending
? null
: () => add.mutate('Task ${DateTime.now().second}'),
child: const Icon(Icons.add),
),
);
}
}

Running it​

cd packages/query_kit_flutter/example && flutter run

Or copy the file into your own project as lib/main.dart after flutter pub add query_kit_flutter.