Fuery Core
Server state caching for Dart: queries, infinite queries, and mutations. Types are inferred from your query and mutation functions, so you don't write type arguments.
This is the pure Dart core. For Flutter apps, use fuery, which re-exports this package and adds widgets. Use fuery_core directly for Dart servers, CLIs, or packages that shouldn't depend on Flutter.
Install
dart pub add fuery_core
Queries
Query.use returns a QueryObserver. It fetches when it gets its first listener, and shares one cache entry and one request with every other observer of the same key.
import 'package:fuery_core/fuery_core.dart';
final todos = Query.use(
queryKey: ['todos'],
queryFn: (context) => api.getTodos(),
staleTime: const Duration(minutes: 1),
);
final subscription = todos.stream.listen((result) {
if (result.isSuccess) print(result.data);
});
await todos.refetch();
await subscription.cancel(); // stops observing; the cache is freed after gcTime
The stream sends the current QueryResult first, then every change. You can also read todos.result at any time, or use subscribe(listener), which returns an unsubscribe function.
To poll until something finishes, combine refetchInterval with refetchWhile:
final job = Query.use(
queryKey: ['jobs', id],
queryFn: (_) => api.getJob(id),
refetchInterval: const Duration(seconds: 2),
refetchWhile: (state) => state.data?.isDone != true,
);
streamedQuery folds a Stream that ends into the query data, and the query succeeds with the first chunk:
final answer = Query.use(
queryKey: ['answer', question],
queryFn: streamedQuery(
stream: (context) => api.ask(question),
initialValue: '',
combine: (text, token) => text + token,
),
);
Mutations
final addTodo = Mutation.use(
mutationFn: (String title) => api.addTodo(title),
onSuccess: (todo, title, context) =>
Fuery.client.invalidateQueries(queryKey: ['todos']),
);
final todo = await addTodo.mutateAsync('Buy milk'); // throws on error
addTodo.mutate('Buy milk'); // reports errors in addTodo.result instead
Infinite queries
final posts = InfiniteQuery.use(
queryKey: ['posts'],
queryFn: (context) => api.getPosts(page: context.pageParam),
initialPageParam: 1,
getNextPageParam: (data) =>
data.lastPage.hasMore ? data.lastPageParam + 1 : null,
);
posts.stream.listen((result) => print(result.pages));
await posts.fetchNextPage();
QueryClient
Fuery.client is the default QueryClient, used whenever no client: is passed. Assign your own to change defaults:
Fuery.client = QueryClient(
defaultOptions: const DefaultOptions(
queries: QueryDefaults(staleTime: Duration(seconds: 30)),
),
);
Fuery.client.invalidateQueries(queryKey: ['todos']);
Fuery.client.setQueryData(['todos', 1], todo);
final data = await Fuery.client.query(
QueryOptions(queryKey: ['todos'], queryFn: (_) => api.getTodos()),
);
To keep data across restarts, give the client a QueryStorage and add persist to a query. A mutation takes persist: MutationPersist(...) and a mutationKey the same way, and client.restore(mutations: [...]) runs the ones that were still waiting when the process ended:
Fuery.client = QueryClient(storage: myStorage); // your QueryStorage, see the persistence guide
final todos = Query.use(
queryKey: ['todos'],
queryFn: (_) => api.getTodos(),
persist: QueryPersist(
toJson: (todos) => [for (final todo in todos) todo.toJson()],
fromJson: (json) => [
for (final item in json! as List)
Todo.fromJson(item as Map<String, Object?>),
],
),
);
Fuery.client.watch turns any value computed from the client into a Stream, without fetching anything:
Fuery.client.watch((client) => client.isFetching()).listen(print);
A mounted client refetches when focusManager or onlineManager report that the app is focused or back online. Assigning Fuery.client mounts the new client; a client you pass as client: yourself, for example in a test, needs client.mount(). Pure Dart has no focus or connectivity events, so set them yourself with setEventListener, or call setFocused and setOnline.
Cached queries keep garbage collection timers running, which keeps a Dart process alive. When a CLI is done, cancel its subscriptions, then call Fuery.client.clear().
Documentation
The fuery README covers the options, optimistic updates, cancellation, and defaults, and the documentation has a guide for each topic.
Acknowledgements
Fuery's caching and refetching model is inspired by TanStack Query.
Libraries
- fuery_core
- Server state caching for Dart: queries, infinite queries, and mutations.