Getting started
By the end of this page a screen shows a list from an API, with loading and error states, and the data cached for every other screen that needs it.
1. Install Fuery
Section titled “1. Install Fuery”flutter pub add fueryfuery includes fuery_core, so this is the only package you need in a Flutter app. For Dart code without Flutter, such as a server or CLI, use dart pub add fuery_core instead.
It needs Dart 3.6 and Flutter 3.27 or newer. There is no native code and no platform setup: fuery_core depends only on clock, collection, and meta, so it runs on every platform Flutter targets, the web included.
2. Write your first query
Section titled “2. Write your first query”A query needs a key that identifies the data and a query function that fetches it. Create the query once, for example in a State field, and build UI from it with QueryBuilder:
class TodoListScreen extends StatefulWidget { const TodoListScreen({super.key});
@override State<TodoListScreen> createState() => _TodoListScreenState();}
class _TodoListScreenState extends State<TodoListScreen> { final todos = Query.use( queryKey: ['todos'], queryFn: (_) => api.getTodos(), );
@override Widget build(BuildContext context) { return QueryBuilder( query: todos, builder: (context, state) => switch (state) { QueryResult(:final data?) => TodoList(data), QueryResult(:final error?) => Text('$error'), _ => const CircularProgressIndicator(), }, ); }}api.getTodos() is any function that returns a Future<List<Todo>>, and TodoList is your own widget that takes the list.
3. Test it
Section titled “3. Test it”A widget test pumps the screen, waits for the fake request, and checks the list. End it by unmounting the tree and emptying the cache: a cached query keeps a timer for its garbage collection, and testWidgets fails on any timer that outlives the test. addTearDown runs too late for that check, so the two lines go at the end of the test body:
testWidgets('shows todos', (tester) async { await tester.pumpWidget(const MaterialApp(home: TodoListScreen())); expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pump(const Duration(milliseconds: 300)); // the fake request expect(find.text('Buy milk'), findsOneWidget);
await tester.pumpWidget(const SizedBox()); Fuery.client.clear();});Testing has the same for cubits and plain Dart, and how to turn retries off so failures show up at once.
What Fuery does for you
Section titled “What Fuery does for you”- No type arguments.
todosis aQueryObserver<List<Todo>>becauseapi.getTodos()returns aFuture<List<Todo>>, andstatein the builder is aQueryResult<List<Todo>>. Mutations, infinite queries, and every widget infer their types the same way. - No null checks.
QueryResult(:final data?)matches only when there is data, sodatais aList<Todo>. That branch comes first, so a list that fails to refresh stays on screen and the error shows only when there is nothing to show. - Creating the query starts nothing. The fetch begins when
QueryBuildermounts, and the first frame already shows loading. - Widgets share data by key. Another screen that uses
['todos']gets the cached list immediately and shares the same request. - Stale data refreshes itself. It refetches in the background when another screen starts using it and when the app returns to the foreground. The old data stays on screen while that happens.
Next steps
Section titled “Next steps”- Server state in Flutter: why server data needs a cache rather than another state class.
- Queries: keys, freshness, and results.
- Widgets: builders, listeners, and consumers.
- Mutations: changing server data and optimistic updates.
- Using with bloc: the same queries inside cubits and blocs.
- Devtools: see every query and mutation while you develop.