Getting Started

Migrating from Other Libraries

Map flutter_query and cached_query concepts to Qora and migrate one screen at a time.

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.

API details for the other libraries were checked against 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

Conceptflutter_queryQora
ProviderQueryClientProvider(client: QueryClient())QoraScope(client: QoraClient())
Query (hooks)useQuery(key, (context) => fn())useQuery(key: key, fetcher: fn)
Query (widget)not providedQoraBuilder<T>
MutationuseMutation((vars, context) => fn(vars))useMutation(mutator: fn) or QoraMutationBuilder
Freshness windowstaleDurationQoraOptions.staleTime
Inactive cache retentiongcDurationQoraOptions.gcTime
Placeholder / seedplaceholder, seedplaceholderData, initialData
Invalidate (prefix, default)client.invalidateQueries(queryKey: ['todos'])client.invalidatePrefix(['todos'])
Invalidate (exact)invalidateQueries(queryKey: k, exact: true)client.invalidate(k)
Read / write cachegetQueryData, setQueryDatagetQueryData, 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

Conceptcached_queryQora
Global configCachedQuery.instance.config(...)QoraClient(config: QoraClientConfig(...))
QueryQuery(key: 'user', queryFn: fn)client.watchQuery(key: ['user'], fetcher: fn)
Streamquery.stream (QueryStatus)watchQuery / watchState (QoraState)
One-shot readawait query.resultawait client.fetchQuery(key: ..., fetcher: ...)
WidgetQueryBuilderQoraBuilder<T>
Force refetchquery.refetch()client.invalidate(key) (active watchers refetch)
Invalidate manyCachedQuery.instance.invalidateCache(...)invalidatePrefix, invalidateWhere, invalidateTags
Freshness / retentionstaleDuration, cacheDurationstaleTime, gcTime
Infinite listInfiniteQuery<T, Arg>InfiniteQueryBuilder<TData, TPageParam>
MutationMutation(mutationFn: fn, invalidateQueries: [...])QoraMutationBuilder / useMutation with MutationOptions.invalidates
Persistencecached_storage (sqflite)PersistQoraClient + your StorageAdapter
Refetch on resume / reconnectrefetchOnResume, refetchOnConnectionQoraScope.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) and FetchStatus (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.

Copyright © 2026