FAQ
Core API
What is the difference between fetchQuery and watchQuery?
fetchQuery<T> executes a one-time fetch and returns a Future<T>. It does not subscribe to state changes.
watchQuery<T> returns a Stream<QoraState<T>> that emits on every state transition. It triggers a fetch on the first subscription and handles stale-while-revalidate, deduplication, and lifecycle events automatically. Use watchQuery whenever a widget needs to reflect live cache state.
What is the difference between getQueryData and getQueryState?
getQueryData<T>(key) returns T?: the raw data value from the current cache entry, or null if no data is available.
getQueryState<T>(key) returns QoraState<T>: the full sealed state including the variant (Initial, Loading, Success, Failure) and all associated fields such as updatedAt and previousData.
Use getQueryData when you only need the data value (for example, to snapshot before an optimistic write). Use getQueryState when you need the full state for display or branching logic.
What is the difference between restoreQueryData and setQueryData?
setQueryData<T>(key, T data) requires a non-null T. It always produces a Success state with updatedAt: DateTime.now().
restoreQueryData<T>(key, T? data) accepts T?. When data is null, it calls removeQuery(key), which restores Initial state. When data is non-null, it calls setQueryData(key, data). This makes restoreQueryData the correct choice for rollback: the snapshot from getQueryData may be null when there was no prior data, and passing that null to restoreQueryData removes the optimistic entry cleanly.
What is the difference between invalidate, invalidateWhere, and invalidateQueries?
invalidate(key) marks a single specific key as stale.
invalidateWhere(bool Function(List<dynamic> key) predicate) marks every cached key that satisfies the predicate as stale. Use this to invalidate a family of related keys with one call (for example, all keys starting with 'users').
invalidateQueries({required QueryFilter filter}) is the typed counterpart to invalidateWhere, accepting a QueryFilter object rather than a raw predicate.
All three trigger a background refetch for every currently-active subscriber on the affected keys.
What is the difference between removeQuery and invalidate?
invalidate(key) marks the entry as stale and triggers a background refetch for active subscribers. The entry remains in cache.
removeQuery(key) deletes the entry from cache entirely. Active subscribers transition to Initial. On PersistQoraClient, it also deletes the persisted entry from StorageAdapter.
What is QoraStatus and when should I use it over the sealed class?
QoraStatus is a plain enum (initial | loading | success | error) accessible via state.status. It is useful when branching logic does not need access to the state's associated fields (for example, toggling a spinner via state.status.isLoading). When you need data, error, previousData, or updatedAt, pattern-match the sealed class directly.
Cache Semantics
What is the difference between staleTime and gcTime?
staleTime controls freshness. Data within the freshness window is served from cache without a network call. After staleTime elapses, data is still served from cache, but a background revalidation is triggered simultaneously (stale-while-revalidate).
gcTime controls eviction. When the last watchQuery subscriber cancels, a GC timer is started. If no new subscriber attaches before gcTime elapses, the entry is removed from cache. The next access starts from Initial.
A common configuration: staleTime < gcTime. Data goes stale and triggers background revalidation, but the entry survives long enough that navigating back to a screen is instant.
When does a QoraBuilder refetch on mount?
By default (refetchOnMount: true at the QoraClientConfig level), watchQuery triggers a fetch on the first subscription regardless of existing cache state.
When refetchOnMount: false (set per-query via QoraOptions or globally via QoraClientConfig), watchQuery only fetches when the entry is in Initial state. If any cached state exists, including a stale Success, no fetch is triggered on mount.
What happens when maxCacheSize is reached?
When the cache reaches maxCacheSize, Qora evicts the least-recently-used inactive entry (an entry with no active watchQuery subscribers). Active entries are never evicted by LRU pressure. The onCacheEvict callback in QoraClientConfig is invoked for each evicted entry.
Flutter Integration
What is the difference between QoraBuilder and QoraStateBuilder?
QoraBuilder<T> calls watchQuery on mount, which triggers a fetch and subscribes to state changes. It takes a 3-parameter builder: Widget Function(BuildContext, QoraState<T>, FetchStatus).
QoraStateBuilder<T> calls watchState instead. It observes existing cache state but never triggers a fetch. It takes a 2-parameter builder: Widget Function(BuildContext, QoraState<T>). Use it when another widget owns the fetch lifecycle and this widget should only display the result.
How do I get a global loading indicator?
With qora_hooks, use useIsFetching(). It returns true while any query has a request in flight and rebuilds only when the value crosses the zero boundary:
class AppProgressBar extends HookWidget {
const AppProgressBar({super.key});
@override
Widget build(BuildContext context) {
final isFetching = useIsFetching();
return AnimatedOpacity(
opacity: isFetching ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
child: const LinearProgressIndicator(minHeight: 2),
);
}
}
Without hooks, subscribe to client.fetchingCountStream directly.
Do I need FlutterLifecycleManager and FlutterConnectivityManager?
Both are optional. Without FlutterLifecycleManager, queries do not refetch when the app returns to the foreground. Without FlutterConnectivityManager, queries do not pause when offline and do not replay on reconnect: all fetches always proceed regardless of connectivity status.
For offline-aware apps, provide both. For apps running in controlled environments (dashboards, internal tools), omitting them is acceptable.
FlutterConnectivityManager takes no constructor parameters. FlutterLifecycleManager requires qoraClient: client.
Can I use Qora without Flutter?
Yes. The qora package is pure Dart with no Flutter dependency. QoraClient, QoraState, QoraOptions, MutationController, and PersistQoraClient are all available in non-Flutter environments (Dart CLI, server-side, tests).
The qora_flutter package adds QoraScope, QoraBuilder, QoraMutationBuilder, InfiniteQueryBuilder, FlutterLifecycleManager, FlutterConnectivityManager, and NetworkStatusIndicator.
Persistence
Do I need to call StorageAdapter.init() before hydrate()?
Yes. Call await storage.init() before calling await client.hydrate(). Calling hydrate() before init() results in undefined behavior for adapters that open file handles or database connections during initialization.
The correct startup sequence:
await storage.init();
client.registerSerializer<User>(userSerializer, name: 'User');
await client.hydrate();
runApp(...);
Why is my serializer name important?
The name string is stored alongside each persisted entry as a type discriminator. During hydrate(), Qora uses it to find the correct fromJson converter. If name is omitted, it defaults to T.toString(), which is mangled by Dart obfuscation (--obfuscate) and minification in Flutter Web builds.
Always pass an explicit name for types that will be persisted in release builds. Keep the name stable across app versions: changing it invalidates all previously persisted entries for that type.
Mutations
Should I use QoraMutationBuilder or useMutation?
QoraMutationBuilder works in any StatelessWidget or StatefulWidget. useMutation requires HookWidget. Both create and dispose a MutationController tied to the widget lifecycle.
On screens that combine a query and a mutation, useMutation alongside useQuery avoids nested builder callbacks and reduces indentation depth. On screens with only a mutation or in codebases that do not use flutter_hooks, QoraMutationBuilder is the correct choice.
When does onSuccess run for offline-queued mutations?
onSuccess runs only when the mutation completes with a real server response (isOptimistic: false). It does not run when an optimistic response is applied locally while offline. This ensures that cache invalidation via invalidate or setQueryData happens only after the server has confirmed the write.
DevTools
Which DevTools surface should I use?
The in-app overlay (qora_devtools_overlay) is stable and available today. It renders inside the running app, and renders nothing in release builds.
A Flutter DevTools extension is in development and not published. Use the overlay.
Why is the overlay empty?
Confirm that the app runs in debug mode (flutter run, not --release or --profile): the overlay builds nothing otherwise. Then confirm that the same OverlayTracker instance is passed to QoraClient(tracker: ...) and to QoraInspector(tracker: ...). A client created without the tracker emits no events.
Case Study: Complete Todo App
A production-grade Flutter app combining every Qora capability: PersistQoraClient, infinite scroll with pagination, optimistic create/toggle/delete, offline mutation queue, and simulated connectivity toggling.
Troubleshooting
Diagnose and resolve common Qora issues, including persistence failures, blocked queries, missing refetches, and repeated widget subscriptions.