# Simple

> One query read in build, its three states told apart with a switch, and a refetch that keeps the data on screen.

The smallest useful screen: one widget reads one query while it builds,
switches over the sealed result to draw a skeleton, an error or the post, and
offers a refresh button that fetches again in the background while the old
post stays visible. It is the shape of every "show one record" screen in an
app, a profile header, an order summary, a settings page loaded from the
server, before anything else is layered on.

Live demo: [Simple](https://dualmeta-gmbh.github.io/query_kit/demo/showcase/#/simple), running in the browser against an in-memory backend ([source](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/simple)). One query, its states, and a refetch.

## What to try

- Watch the skeleton in the *Post #1* card give way to the post, *Local
  development: setup guide*: one request, and the debug strip
  under the card reads `status=success` and `fetches=1`.
- Press the refresh icon. A *refreshing* pill appears beside it while the
  fetch runs, the post never leaves the screen, and `fetches` goes up by one.
- Press it several times in a row: the button is disabled while a fetch is
  running, so each press is one request.

## The code

The query is a function returning options, so its `queryFn` can close over
the screen's api client; the key is what the cache stores the post under.

[`examples/showcase/lib/features/simple/simple_screen.dart`, lines 33–37](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/examples/showcase/lib/features/simple/simple_screen.dart#L33-L37):

```dart
QueryObserverOptions<Post> firstPostQuery(ShowcaseApi api) =>
    QueryObserverOptions<Post>(
      queryKey: ShowcaseKeys.post(1),
      queryFn: (context) => api.post(1, signal: context.signal),
    );
```

The screen reads it with `context.query` and switches over the result. A
refetch that fails keeps the last good data (`QueryError(staleData: …)`), so
the error pattern with data and the success pattern share one branch.

[`examples/showcase/lib/features/simple/simple_screen.dart`, lines 46–96](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/examples/showcase/lib/features/simple/simple_screen.dart#L46-L96):

```dart
final post = context.query(firstPostQuery(api));

return FeatureScaffold(
  feature: simpleFeature,
  children: <Widget>[
    SectionCard(
      title: 'Post #1',
      trailing: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          if (post.isFetching) const Pill('refreshing'),
          IconButton(
            tooltip: 'Refetch',
            onPressed: post.isFetching ? null : post.refetch,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      child: switch (post) {
        QueryPending() => const Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              SkeletonBox(height: 20, width: 240),
              SizedBox(height: 8),
              SkeletonBox(),
              SizedBox(height: 4),
              SkeletonBox(),
            ],
          ),
        QueryError(:final error, staleData: null) =>
          Notice('$error', error: true),
        QuerySuccess(:final data) ||
        QueryError(staleData: final data!) =>
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              if (post case QueryError(:final error)) ...<Widget>[
                Notice('Refetch failed: $error', error: true),
                const SizedBox(height: 8),
              ],
              Text(
                data.title,
                style: Theme.of(context).textTheme.titleLarge,
              ),
              const SizedBox(height: 8),
              Text(data.body),
            ],
          ),
      },
    ),
    QueryDebugStrip(queryKey: ShowcaseKeys.post(1), label: 'post'),
```

<details>
<summary>The whole screen</summary>

[`examples/showcase/lib/features/simple/simple_screen.dart`](https://github.com/dualmeta-gmbh/query_kit/blob/d69b05dc391bd0585e2e53600572b6440025a7e9/examples/showcase/lib/features/simple/simple_screen.dart):

```dart
/// Upstream's `simple` example: one query read in build, its states told
/// apart with a `switch` over the sealed result, and a refetch button that
/// shows `isFetching` while the background fetch runs.
///
/// Proofs (widget tests in `test/features/simple_test.dart`, end-to-end in
/// `e2e/tests/simple.spec.ts`): the skeleton gives way to the post after one
/// request; a refetch shows the "refreshing" pill while the data stays on
/// screen and bumps the strip's `fetches`; a refused first fetch ends in the
/// error state after the default retries; a refused refetch keeps the stale
/// data next to the error.
library;

import 'package:flutter/material.dart';
import 'package:query_kit_flutter/query_kit_flutter.dart';

import '../../shared/api.dart';
import '../../shared/chrome.dart';
import '../../shared/debug_strip.dart';
import '../../shared/feature.dart';
import '../../shared/feature_scaffold.dart';
import '../../shared/models.dart';
import '../../shared/scope.dart';

const Feature simpleFeature = Feature(
  id: 'simple',
  title: 'Simple',
  summary: 'One query, its states, and a refetch.',
  upstream: 'simple',
);

/// The screen's one query. The options are a function, not a constant, so
/// the `queryFn` can close over the api; the key is what the cache goes by.
QueryObserverOptions<Post> firstPostQuery(ShowcaseApi api) =>
    QueryObserverOptions<Post>(
      queryKey: ShowcaseKeys.post(1),
      queryFn: (context) => api.post(1, signal: context.signal),
    );

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

  @override
  Widget build(BuildContext context) {
    final api = ShowcaseScope.apiOf(context);
    // Read in build: the widget rebuilds when the result changes.
    final post = context.query(firstPostQuery(api));

    return FeatureScaffold(
      feature: simpleFeature,
      children: <Widget>[
        SectionCard(
          title: 'Post #1',
          trailing: Row(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              if (post.isFetching) const Pill('refreshing'),
              IconButton(
                tooltip: 'Refetch',
                onPressed: post.isFetching ? null : post.refetch,
                icon: const Icon(Icons.refresh),
              ),
            ],
          ),
          child: switch (post) {
            QueryPending() => const Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: <Widget>[
                  SkeletonBox(height: 20, width: 240),
                  SizedBox(height: 8),
                  SkeletonBox(),
                  SizedBox(height: 4),
                  SkeletonBox(),
                ],
              ),
            QueryError(:final error, staleData: null) =>
              Notice('$error', error: true),
            QuerySuccess(:final data) ||
            QueryError(staleData: final data!) =>
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  if (post case QueryError(:final error)) ...<Widget>[
                    Notice('Refetch failed: $error', error: true),
                    const SizedBox(height: 8),
                  ],
                  Text(
                    data.title,
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  const SizedBox(height: 8),
                  Text(data.body),
                ],
              ),
          },
        ),
        QueryDebugStrip(queryKey: ShowcaseKeys.post(1), label: 'post'),
      ],
    );
  }
}
```

</details>

## Related

- Guides: [Queries](https://dualmeta-gmbh.github.io/query_kit/docs/guides/queries.md), [Four ways to read a query](https://dualmeta-gmbh.github.io/query_kit/docs/guides/reading-queries-in-widgets.md)
- Upstream: TanStack's React [`simple`](https://github.com/TanStack/query/tree/main/examples/react/simple) example
- Tested by `test/features/simple_test.dart` (widget) and `e2e/tests/simple.spec.ts` (browser)
- [View the feature on GitHub](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/simple)
