# One-file tour

> The package's own example, a provider, one query read two ways and a mutation that invalidates it, in a single file with no server.

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](https://dualmeta-gmbh.github.io/query_kit/docs/examples/simple.md) and [Mutations](https://dualmeta-gmbh.github.io/query_kit/docs/examples/mutations.md) 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](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/packages/query_kit_flutter/example/lib/main.dart#L14-L37):

```dart
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](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/packages/query_kit_flutter/example/lib/main.dart#L44-L49):

```dart
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](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/packages/query_kit_flutter/example/lib/main.dart#L51-L58):

```dart
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](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/packages/query_kit_flutter/example/lib/main.dart#L60-L129):

```dart
class TasksScreen extends StatelessWidget {
  const TasksScreen({super.key});

  @override
  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

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

## Related

- [Installation](https://dualmeta-gmbh.github.io/query_kit/docs/installation.md) and [Quick start](https://dualmeta-gmbh.github.io/query_kit/docs/quick-start.md)
- [Four ways to read a query](https://dualmeta-gmbh.github.io/query_kit/docs/guides/reading-queries-in-widgets.md)
- [Mutations](https://dualmeta-gmbh.github.io/query_kit/docs/guides/mutations.md) and [Query invalidation](https://dualmeta-gmbh.github.io/query_kit/docs/guides/query-invalidation.md)
- [View on GitHub](https://github.com/dualmeta-gmbh/query_kit/tree/main/packages/query_kit_flutter/example)
