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