Skip to content

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.

Terminal window
flutter pub add fuery

fuery 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.

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.

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.

  • No type arguments. todos is a QueryObserver<List<Todo>> because api.getTodos() returns a Future<List<Todo>>, and state in the builder is a QueryResult<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, so data is a List<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 QueryBuilder mounts, 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.
  • 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.