Simple
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.
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=successandfetches=1. - Press the refresh icon. A refreshing pill appears beside it while the
fetch runs, the post never leaves the screen, and
fetchesgoes 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.
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.
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'),
The whole screen
/// 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});
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'),
],
);
}
}
Related
- Guides: Queries, Four ways to read a query
- Upstream: TanStack's React
simpleexample - Tested by
test/features/simple_test.dart(widget) ande2e/tests/simple.spec.ts(browser) - View the feature on GitHub