API Reference

QoraOptions

Per-query configuration that overrides QoraClientConfig defaults. Pass to fetchQuery or watchQuery to customise caching, retries, staleness, and polling.

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.

user_repository.dart
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.

main.dart
// 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 typeRecommended value
Live prices, feedsDuration.zero
User profileDuration(minutes: 5)
Article / post contentDuration(minutes: 10)
App configurationDuration(hours: 1)
Static reference dataDuration(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.

main.dart
// 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:

  • fetchQuery throws a StateError immediately
  • watchQuery emits the current state without triggering any fetch

Useful for conditional queries that should only run when a prerequisite is satisfied:

main.dart
// 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:

APIBehaviour when dependency unresolved
watchQuerySubscribes to the dependency stream; fires automatically when data arrives
fetchQueryThrows StateError immediately
prefetchSilently skips
main.dart
// 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.

main.dart
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.

main.dart
// 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:

main.dart
// 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.

dashboard_page.dart
// 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:

ValueBehaviour
trueAlways refetch on mount, even if data is fresh
falseOnly fetch if data is missing; skip if cached
nullUse QoraClientConfig.refetchOnMount (default: true)

networkMode

final NetworkMode networkMode

Default: NetworkMode.online

Controls how this query behaves when the device has no connectivity:

ValueBehaviour
NetworkMode.onlineFetch is paused while offline (FetchStatus.paused). Replayed automatically on reconnect.
NetworkMode.alwaysFetch always runs regardless of connectivity. Use for local-database or in-memory fetchers.
NetworkMode.offlineFirstCached data is returned immediately. A background revalidation is queued for when online.
lib/data/user_repository.dart
// 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),
Set 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:

main.dart
// 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.

post_list.dart
// 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.

user_repository.dart
// 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.

post_repository.dart
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:

post_repository.dart
// 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:

  1. Data that is already Map, List, String, num, bool, or null: passed through as-is.
  2. Any object with a toJson() method (json_serializable, freezed, manual): called dynamically.
  3. 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.

user_repository.dart
// 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

main.dart
// 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)
Copyright © 2026