QoraOptions
Overview
QoraOptions configures the behaviour of an individual query. Any field left at its default inherits from QoraClientConfig.defaultOptions. Fields are merged with per-query values taking priority.
await client.fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(
staleTime: Duration(minutes: 5),
retryCount: 2,
),
);
Constructor
const QoraOptions({
Duration staleTime = Duration.zero,
Duration gcTime = const Duration(minutes: 5),
bool enabled = true,
Object? dependsOn,
int retryCount = 3,
Duration retryDelay = const Duration(seconds: 1),
Duration Function(int attemptIndex)? retryDelayCalculator,
bool Function(Object error, int attemptIndex)? retryCondition,
bool refetchOnWindowFocus = true,
bool refetchOnReconnect = true,
Duration? refetchInterval,
bool? refetchOnMount,
NetworkMode networkMode = NetworkMode.online,
Object? Function(Object? data)? toJson,
})
Fields
staleTime
final Duration staleTime
Default: Duration.zero
How long data is considered fresh after a successful fetch. During the freshness window, fetchQuery and watchQuery skip the network entirely and return cached data.
Once staleTime elapses the data is considered stale: Qora returns it immediately but also triggers a background revalidation.
// Always stale (default): serve cache, always revalidate
staleTime: Duration.zero,
// Fresh for 5 minutes: no network in the window
staleTime: Duration(minutes: 5),
// Effectively never stale
staleTime: Duration(days: 365),
| Data type | Recommended value |
|---|---|
| Live prices, feeds | Duration.zero |
| User profile | Duration(minutes: 5) |
| Article / post content | Duration(minutes: 10) |
| App configuration | Duration(hours: 1) |
| Static reference data | Duration(days: 1) |
gcTime
final Duration gcTime
Default: Duration(minutes: 5)
How long an inactive entry stays in cache after all watchQuery subscribers cancel. When the GC timer fires the entry is removed; the next access starts from Initial.
// Keep in cache for 10 minutes after leaving the screen
gcTime: Duration(minutes: 10),
// Remove immediately on unsubscribe (e.g. sensitive session data)
gcTime: Duration.zero,
staleTime and gcTime are independent. A common pattern is staleTime < gcTime: data goes stale (triggering a revalidation) but stays in cache so navigation back is instant.enabled
final bool enabled
Default: true
When false:
fetchQuerythrows aStateErrorimmediatelywatchQueryemits the current state without triggering any fetch
Useful for conditional queries that should only run when a prerequisite is satisfied:
// Only fetch when userId is non-null
options: QoraOptions(enabled: userId != null),
dependsOn
final Object? dependsOn
Default: null
The key of another query that must have Success data before this query fires. Provides a reactive dependency without manual coordination:
| API | Behaviour when dependency unresolved |
|---|---|
watchQuery | Subscribes to the dependency stream; fires automatically when data arrives |
fetchQuery | Throws StateError immediately |
prefetch | Silently skips |
// Step 1: fetch the session (no dependency)
client.watchQuery<Session>(
key: ['session'],
fetcher: () => api.getSession(),
);
// Step 2: fetch the user only once session has data
client.watchQuery<User>(
key: ['user'],
fetcher: () => api.getUser(session!.userId),
options: QoraOptions(dependsOn: ['session']),
);
See the Dependent Queries guide for full examples.
retryCount
final int retryCount
Default: 3
Number of automatic retry attempts after a fetch failure. Set to 0 to disable retries.
retryCount: 0, // fail immediately on first error
retryCount: 5, // retry up to 5 times
retryDelay
final Duration retryDelay
Default: Duration(seconds: 1)
Base delay between retry attempts. By default Qora uses exponential backoff:
- Attempt 0 →
1 s - Attempt 1 →
2 s - Attempt 2 →
4 s - Attempt 3 →
8 s
retryDelayCalculator
final Duration Function(int attemptIndex)? retryDelayCalculator
Default: null (uses exponential backoff)
Custom delay calculator. Receives the zero-based attempt index and returns the delay before that attempt.
// Fixed 500 ms delay
retryDelayCalculator: (_) => const Duration(milliseconds: 500),
// Jittered exponential backoff
retryDelayCalculator: (i) => Duration(
milliseconds: (500 * pow(2, i) + Random().nextInt(200)).toInt(),
),
retryCondition
final bool Function(Object error, int attemptIndex)? retryCondition
Default: null (retry all failures up to retryCount)
Predicate that decides whether a failed fetch should be retried. Receives the error and the zero-based attempt index that just failed. Return false to skip further retries.
Useful to avoid retrying non-recoverable errors like 4xx HTTP responses:
// Only retry network errors, not application errors
retryCondition: (error, attempt) => error is SocketException,
// Never retry 4xx responses (assuming you map them to a typed exception)
retryCondition: (error, attempt) => error is! HttpException,
// Retry only the first attempt, fail fast on subsequent failures
retryCondition: (error, attempt) => attempt == 0,
When retryCondition returns false, all remaining retries for that error are skipped and the error is thrown immediately.
refetchOnWindowFocus
final bool refetchOnWindowFocus
Default: true
Whether to refetch this query when the app regains window focus. Requires a LifecycleManager to be registered on the client.
refetchOnReconnect
final bool refetchOnReconnect
Default: true
Whether to refetch this query when the network connection is restored. Requires a ConnectivityManager to be registered on the client.
refetchInterval
final Duration? refetchInterval
Default: null (disabled)
When set, watchQuery automatically refetches at this cadence while at least one subscriber is active. The timer stops when the last subscriber cancels.
// Poll every 30 seconds while the widget is mounted
options: QoraOptions(refetchInterval: Duration(seconds: 30)),
refetchOnMount
final bool? refetchOnMount
Default: null (falls back to QoraClientConfig.refetchOnMount)
Controls whether watchQuery triggers a fetch on the first subscription:
| Value | Behaviour |
|---|---|
true | Always refetch on mount, even if data is fresh |
false | Only fetch if data is missing; skip if cached |
null | Use QoraClientConfig.refetchOnMount (default: true) |
networkMode
final NetworkMode networkMode
Default: NetworkMode.online
Controls how this query behaves when the device has no connectivity:
| Value | Behaviour |
|---|---|
NetworkMode.online | Fetch is paused while offline (FetchStatus.paused). Replayed automatically on reconnect. |
NetworkMode.always | Fetch always runs regardless of connectivity. Use for local-database or in-memory fetchers. |
NetworkMode.offlineFirst | Cached data is returned immediately. A background revalidation is queued for when online. |
// Default: pause while offline, replay on reconnect
options: const QoraOptions(networkMode: NetworkMode.online),
// Local DB query: always execute, never pause
options: const QoraOptions(networkMode: NetworkMode.always),
// Serve cache immediately, refresh in background when online
options: const QoraOptions(networkMode: NetworkMode.offlineFirst),
NetworkMode.always for queries backed by SQLite, Hive, or any local store: they do not need connectivity and should never be paused.structuralSharing
final bool structuralSharing
Default: true
Preserves referential equality for unchanged nested data across fetches. When enabled, every successful fetch compares the new data against the existing cache using deep equality. If the data is structurally unchanged, the previous data reference is retained.
This prevents unnecessary widget rebuilds: widgets that compare with identical() or == can skip re-rendering when the data hasn't actually changed.
The comparison is recursive for Map, List, and Set. Custom model classes should override == and hashCode for reliable detection.
Disable this only if you have very large data structures and the deep-equality check becomes a measured bottleneck:
// Enabled (default): preserve references when data has not changed
QoraOptions(structuralSharing: true),
// Disabled: always create new references on every fetch
QoraOptions(structuralSharing: false),
keepPreviousData
final bool keepPreviousData
Default: false
When true, the previous data stays visible while the next fetch is in progress, suppressing the Success → Loading → Success transition. The entry stays in Success state during the fetch and on fetch failure, so the UI never flashes a loading spinner.
Useful for offset-based pagination: keep the current page visible while fetching the next one.
// Page changes without a loading flash
fetchQuery<List<Post>>(
key: ['posts', 'page', page],
fetcher: () => api.getPosts(page: page, limit: 20),
options: QoraOptions(
keepPreviousData: true,
staleTime: Duration.zero,
),
);
Has no effect on the initial fetch (no previous data to keep).
transform
final Object Function(Object? data)? transform
Default: null
Transform function applied to successful fetch data before it enters the cache. Use this to decouple data transformation from the fetcher: the fetcher returns raw data (e.g. JSON-decoded maps) and transform converts it into the final model.
The transform runs before structural sharing, so changes to the transformed data are correctly detected against the cached entry.
// Fetcher returns raw JSON; transform converts it to a model class.
fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId), // returns Map<String, dynamic>
options: QoraOptions(
transform: (raw) => User.fromJson(raw as Map<String, dynamic>),
),
);
Both the fetcher and the transform are independently testable: the fetcher returns raw data, the transform is a pure function.
transformError
final Object Function(Object error)? transformError
Default: null
Transform function applied to fetch errors before they enter the cache. Receives the raw error thrown by the fetcher (after all retries). Use it to normalise errors across your API layer or to enrich them with domain context.
Runs before QoraClientConfig.errorMapper, so the per-query transform takes precedence for specific queries before the global mapper.
fetchQuery<Post>(
key: ['post', postId],
fetcher: () => api.getPost(postId),
options: QoraOptions(
transformError: (error) => switch (error) {
DioException(e: final err) => AppError.fromDio(err),
_ => AppError.unknown(error.toString()),
},
),
);
providesTags
final List<QueryTag>? providesTags
Default: null
Tags that this query provides for tag-based cache invalidation. When a mutation invalidates a matching tag (via MutationOptions.invalidatesTags), all queries that provide that tag are automatically refetched, regardless of their query key.
This decouples invalidation logic from query-key structure, making it easier to invalidate related queries across different key patterns:
// Query provides tags
watchQuery<Post>(
key: ['post', postId],
fetcher: () => api.getPost(postId),
options: QoraOptions(
providesTags: [
QueryTag('post', postId.toString()),
QueryTag('author', authorId.toString()),
],
),
);
Wildcard matching
Invalidating QueryTag('post') (no id) refetches ALL queries that provide any tag of type 'post' regardless of their individual ids. Invalidating QueryTag('post', '123') only refetches queries that provide the specific 'post:123' tag.
// Refetch ALL queries tagged with 'post' (any id).
client.invalidateTags([QueryTag('post')]);
// Refetch only queries tagged with 'post:123'.
client.invalidateTags([QueryTag('post', '123')]);
toJson
final Object? Function(Object? data)? toJson
Default: null
Optional serializer that controls how query data is displayed in the DevTools overlay.
When omitted, Qora applies the following resolution order automatically:
- Data that is already
Map,List,String,num,bool, ornull: passed through as-is. - Any object with a
toJson()method (json_serializable,freezed, manual): called dynamically. - Everything else:
.toString()fallback.
Provide toJson explicitly only when the automatic detection is insufficient, for example when your toJson() method has a non-standard signature or returns a non-encodable type.
// Explicit serializer (overrides auto-detection)
fetchQuery<User>(
key: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(
toJson: (data) => (data as User).toJson(),
),
);
→ See Displaying Query Data in DevTools for the full guide.
getRetryDelay(int attemptIndex)
Duration getRetryDelay(int attemptIndex)
Computes the retry delay for the given zero-based attempt index. Delegates to retryDelayCalculator if set, otherwise applies exponential backoff: retryDelay × 2^attemptIndex.
merge(QoraOptions? other)
QoraOptions merge(QoraOptions? other)
Returns a new QoraOptions merging this with other. Fields from other take priority. Nullable fields (retryDelayCalculator, refetchInterval, refetchOnMount) fall back to this when not set in other.
Used internally by QoraClient to combine global defaults with per-query options.
Common Recipes
// Live data: always fresh, poll every 10 s
QoraOptions(
staleTime: Duration.zero,
refetchInterval: Duration(seconds: 10),
)
// Config data: cache aggressively
QoraOptions(
staleTime: Duration(hours: 1),
gcTime: Duration(hours: 2),
retryCount: 5,
)
// Conditional query
QoraOptions(enabled: currentUser != null)
// Disable retries for mutations
QoraOptions(retryCount: 0)