Migrating from Other Libraries
This guide maps the concepts of flutter_query and cached_query to Qora. Both libraries are solid; the goal here is not to criticise them but to make a migration predictable.
flutter_query 0.11 and cached_query 3.7. If you are on a different version, double-check names against their own documentation.Migrate incrementally
You do not need a big-bang rewrite. A QoraScope can live next to another provider, so you can migrate one screen or one feature at a time:
void main() {
runApp(
QoraScope.auto(
client: QoraClient(),
child: const MyApp(), // existing providers can stay inside
),
);
}
From flutter_query
flutter_query is hook-based (flutter_hooks) and follows TanStack Query closely. The fastest path is qora_hooks, which keeps the same hook style.
Concept mapping
| Concept | flutter_query | Qora |
|---|---|---|
| Provider | QueryClientProvider(client: QueryClient()) | QoraScope(client: QoraClient()) |
| Query (hooks) | useQuery(key, (context) => fn()) | useQuery(key: key, fetcher: fn) |
| Query (widget) | not provided | QoraBuilder<T> |
| Mutation | useMutation((vars, context) => fn(vars)) | useMutation(mutator: fn) or QoraMutationBuilder |
| Freshness window | staleDuration | QoraOptions.staleTime |
| Inactive cache retention | gcDuration | QoraOptions.gcTime |
| Placeholder / seed | placeholder, seed | placeholderData, initialData |
| Invalidate (prefix, default) | client.invalidateQueries(queryKey: ['todos']) | client.invalidatePrefix(['todos']) |
| Invalidate (exact) | invalidateQueries(queryKey: k, exact: true) | client.invalidate(k) |
| Read / write cache | getQueryData, setQueryData | getQueryData, setQueryData |
flutter_query.invalidateQueries matches by prefix by default. Qora's invalidate(key) is exact. Use invalidatePrefix where you relied on the default behaviour.Reading state
flutter_query exposes boolean flags on a result object. Qora returns a sealed QoraState<T>, so the compiler checks that you handle every case, and stale data stays available during refetches and failures:
// flutter_query
final result = useQuery(const ['user'], (context) => api.getUser());
if (result.isLoading) return const CircularProgressIndicator();
if (result.isError) return Text('Error: ${result.error}');
return UserCard(result.data!);
// Qora (qora_hooks)
final state = useQuery<User>(key: const ['user'], fetcher: api.getUser);
return switch (state) {
Success(:final data) => UserCard(data),
Failure(:final error, :final previousData) =>
previousData != null ? UserCard(previousData) : Text('Error: $error'),
Loading(:final previousData) =>
previousData != null ? UserCard(previousData) : const CircularProgressIndicator(),
Initial() => const SizedBox.shrink(),
};
Mutations and invalidation
// flutter_query
final mutation = useMutation<Todo, Exception, String, void>(
(title, context) => api.createTodo(title),
onSuccess: (data, variables, onMutateResult, context) {
// invalidate ['todos'] through the QueryClient
},
);
// Qora: declare what to refresh, no manual callback needed
final mutation = useMutation<Todo, String>(
mutator: api.createTodo,
options: const MutationOptions(invalidates: [['todos']]),
);
mutation.mutate('Buy milk');
From cached_query
cached_query is object-based: you create Query / Mutation instances (often in a repository) and listen to their streams, with Flutter widgets in cached_query_flutter.
Concept mapping
| Concept | cached_query | Qora |
|---|---|---|
| Global config | CachedQuery.instance.config(...) | QoraClient(config: QoraClientConfig(...)) |
| Query | Query(key: 'user', queryFn: fn) | client.watchQuery(key: ['user'], fetcher: fn) |
| Stream | query.stream (QueryStatus) | watchQuery / watchState (QoraState) |
| One-shot read | await query.result | await client.fetchQuery(key: ..., fetcher: ...) |
| Widget | QueryBuilder | QoraBuilder<T> |
| Force refetch | query.refetch() | client.invalidate(key) (active watchers refetch) |
| Invalidate many | CachedQuery.instance.invalidateCache(...) | invalidatePrefix, invalidateWhere, invalidateTags |
| Freshness / retention | staleDuration, cacheDuration | staleTime, gcTime |
| Infinite list | InfiniteQuery<T, Arg> | InfiniteQueryBuilder<TData, TPageParam> |
| Mutation | Mutation(mutationFn: fn, invalidateQueries: [...]) | QoraMutationBuilder / useMutation with MutationOptions.invalidates |
| Persistence | cached_storage (sqflite) | PersistQoraClient + your StorageAdapter |
| Refetch on resume / reconnect | refetchOnResume, refetchOnConnection | QoraScope.auto() |
Keys
cached_query keys are usually strings. Qora keys are lists, which makes prefix and partial invalidation natural:
// cached_query
Query(key: 'user-$id', queryFn: () => api.getUser(id));
// Qora
client.watchQuery(key: ['users', id], fetcher: () => api.getUser(id));
client.invalidatePrefix(['users']); // every user query
What you gain
- Offline mutation queue: writes made offline are queued and replayed in order on reconnect, with optimistic UI in the meantime.
- Two-axis state:
QoraState(data) andFetchStatus(fetching / paused / idle) are independent, so "offline with stale data" is a first-class state. - Tag-based invalidation: mutations invalidate logical tags instead of hard-coded keys.
- In-app DevTools overlay to inspect the cache, timelines and mutations.
Error handling
Both libraries let you inspect raw errors. In Qora, map them once with QoraClientConfig.errorMapper, which receives the error and its stack trace and returns a QoraException:
import 'package:qora_flutter/qora_flutter.dart';
final client = QoraClient(
config: QoraClientConfig(
errorMapper: (Object error, StackTrace? stackTrace) => error is QoraException
? error
: QoraException(error.toString(), originalError: error, stackTrace: stackTrace),
),
);
See Dio integration for a complete mapper.
Coming from TanStack Query
A precise mapping of TanStack Query concepts, hooks, and configuration options to their Qora equivalents. Covers what is identical, what is renamed, and what works differently.
AI Assistants
Give Claude, Cursor, Copilot or any coding assistant the context it needs to write correct Qora code.