QoraBuilder
QoraBuilder<T> is the primary way to bind server data to Flutter UI. It handles the full query lifecycle: fetching, caching, reactivity, background revalidation: with a single widget declaration.
Basic Usage
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: () => api.getUsers(),
builder: (context, state, fetchStatus) {
return switch (state) {
Initial() => const SizedBox.shrink(),
Loading(:final previousData) => previousData != null
? Stack(children: [UserList(previousData), const LinearProgressIndicator()])
: const CircularProgressIndicator(),
Success(:final data) => UserList(data),
Failure(:final error, :final previousData) => previousData != null
? Column(children: [UserList(previousData), ErrorBanner(error)])
: ErrorScreen(error),
};
},
)
The builder receives three arguments:
context: theBuildContextstate: the currentQoraState<T>(what you have)fetchStatus: the currentFetchStatus(what Qora is doing right now)
Exhaustive pattern-matching over the sealed state is the idiomatic approach. Use fetchStatus for secondary indicators (progress bars, offline chips) without changing the primary state view.
How It Works
On mount, QoraBuilder:
- Subscribes to
client.watchState<T>(queryKey): an observe-only stream that emits every state update for this key (including updates triggered by other widgets or background services). - Fetches via
client.fetchQuery(...)ifenabledistrue. The client handles deduplication, caching, and stale-while-revalidate automatically.
When the query is externally invalidated (via client.invalidate(), a lifecycle resume, or a reconnect event), the state transitions to Loading(previousData: data). QoraBuilder detects this pattern in the stream and dispatches a fresh fetchQuery. The client's deduplication ensures only one network request fires even if multiple widgets are subscribed to the same key.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
queryKey | Object (List or QoraKey) | required | Cache key. Changing it triggers re-subscription and a fresh fetch. |
fetcher | Future<T> Function() | required | Async function that returns the data. Must throw on failure. |
builder | Widget Function(BuildContext, QoraState<T>, FetchStatus) | required | Builds the widget tree from the current state and fetch status. |
options | QoraOptions? | null | Per-query config merged on top of QoraClientConfig.defaultOptions. |
enabled | bool | true | When false, no fetch is triggered. The widget still observes the current cached state. |
keepPreviousData | bool | false | Augments Loading/Failure states with the last known Success data from this widget. |
select | Object? Function(T data)? | null | Rebuild filter: the widget rebuilds only when the state kind or this value changes. The builder still receives the full state. |
client | QoraClient? | null | Override the client from QoraScope. Useful for testing or nested scopes. |
Patterns
Handling all four states
Use .when() for exhaustive matching or .maybeWhen() when you only care about specific states:
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => api.getUser(userId),
builder: (context, state, fetchStatus) {
return state.when(
onInitial: () => const SizedBox.shrink(),
onLoading: (previousData) => previousData != null
? UserCard(user: previousData, isRefreshing: true)
: const CircularProgressIndicator(),
onSuccess: (user, updatedAt) => UserCard(user: user),
onError: (error, stackTrace, previousData) => ErrorScreen(
message: error.toString(),
onRetry: () => context.qora.invalidate(['users', userId]),
),
);
},
)
Offline-aware UI with FetchStatus
FetchStatus is the second axis alongside QoraState. Use it to show a background-refresh indicator or an "offline: waiting" message without changing the primary state view:
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: api.getUsers,
builder: (context, state, fetchStatus) {
return switch (state) {
Initial() || Loading(previousData: null)
when fetchStatus == FetchStatus.paused =>
const Center(child: Text('Offline: connect to load')),
Initial() || Loading() =>
const CircularProgressIndicator(),
Success(:final data) => Column(
children: [
if (fetchStatus == FetchStatus.fetching)
const LinearProgressIndicator(),
if (fetchStatus == FetchStatus.paused)
const OfflineSyncChip(),
UserList(data),
],
),
Failure(:final error) => ErrorScreen(error),
};
},
)
FetchStatus value | Meaning |
|---|---|
FetchStatus.fetching | A network request is in-flight. |
FetchStatus.paused | Offline: fetch is queued and will replay on reconnect. |
FetchStatus.idle | No active fetch (data is fresh or the state is terminal). |
FetchStatus.paused only appears when QoraOptions.networkMode is NetworkMode.online (the default) and the device has no connectivity. If you use NetworkMode.always, fetches run regardless of connectivity and this state is never emitted.Conditional queries (enabled)
Prevent fetching until a dependency is available:
QoraBuilder<Profile>(
queryKey: ['profiles', userId],
fetcher: () => api.getProfile(userId!),
enabled: userId != null, // No fetch until userId is resolved
builder: (context, state, fetchStatus) { ... },
)
When enabled flips from false to true (e.g. via setState), a fetch is triggered automatically.
Performance: derived subscriptions with select
select lets you derive a computed value from the query data. The widget only rebuilds when the selected value changes (compared via ==). Use it to avoid unnecessary rebuilds when only a subset of the data is needed:
// Rebuild only when the user count changes.
QoraBuilder<List<User>>(
queryKey: ['users'],
fetcher: () => api.getUsers(),
select: (users) => users.length,
builder: (context, state, fetchStatus) =>
Text('${state.dataOrNull?.length ?? 0} users'),
)
The builder still receives the full, up-to-date QoraState<T>: select decides when to rebuild, not what is passed. A change of state kind (Loading, Success, Failure) or of FetchStatus always rebuilds, so errors and spinners are never hidden. Without select, the widget rebuilds on every state change.
Flicker-free pagination (keepPreviousData)
When the query key changes (e.g. page number increments), the new key starts in Initial state: causing a jarring full-screen spinner. keepPreviousData prevents this by carrying the last successful data forward into the new key's loading phase:
QoraBuilder<List<Post>>(
queryKey: ['posts', currentPage],
fetcher: () => api.getPosts(page: currentPage),
keepPreviousData: true,
builder: (context, state, fetchStatus) {
// dataOrNull returns data from Success, Loading.previousData, or Failure.previousData
final posts = state.dataOrNull ?? [];
return Stack(
children: [
PostList(posts: posts),
if (state.isLoading) const LinearProgressIndicator(),
],
);
},
)
keepPreviousData only fills in the gap: if the client already supplies previousData in the state, that value takes priority. The widget's local cache is purely a fallback.Displaying a freshness timestamp
Success carries updatedAt: the moment the data was last fetched:
onSuccess: (user, updatedAt) => Column(
children: [
UserCard(user: user),
Text(
'Last updated ${_timeAgo(updatedAt)}',
style: Theme.of(context).textTheme.labelSmall,
),
],
),
Polling
Pass refetchInterval via options to poll on a fixed schedule:
QoraBuilder<List<Notification>>(
queryKey: ['notifications'],
fetcher: api.getNotifications,
options: const QoraOptions(refetchInterval: Duration(seconds: 30)),
builder: (context, state) { ... },
)
Polling is active only while the QoraBuilder is in the widget tree. It stops automatically on dispose.
Seeding initial data (initialData)
QoraOptions.initialData seeds the cache with placeholder data before any fetch completes. Use it for SSR hydration, instant renders from shared preferences, or a previous app session:
QoraBuilder<User>(
queryKey: ['users', userId],
fetcher: () => api.getUser(userId),
options: QoraOptions(
initialData: cachedUser, // pre-seed the cache
initialDataUpdatedAt: DateTime.now(), // mark as fresh
staleTime: const Duration(minutes: 5), // skip network if fresh
),
builder: (context, state, fetchStatus) { ... },
)
| Parameter | Default | Description |
|---|---|---|
initialData | null | Placeholder data of type T to pre-populate the cache. |
initialDataUpdatedAt | epoch | Timestamp for staleness calc. Set to DateTime.now() to skip the network. |
placeholderData | null | Lazy function that derives initial data from another cache entry. |
When initialData is provided:
- If fresh (
updatedAtwithinstaleTime): the widget renders with the placeholder immediately and skips the network. - If stale (default epoch timestamp): the widget renders with the placeholder and triggers a background SWR refetch. The placeholder is carried via
Loading.previousData.
initialData takes precedence over placeholderData. A runtime type mismatch is silently ignored (the cache stays Initial and fetches normally).Key Changing Behaviour
When queryKey changes (didUpdateWidget), QoraBuilder:
- Cancels the previous stream subscription.
- Subscribes to the new key's stream (starting from its current cache state: could be
Initial,Success, etc.). - Dispatches a fresh
fetchQueryfor the new key (ifenabled).
This is how pagination, tab switching, and filter changes are handled automatically without any extra state management.
QoraStateBuilder: Observe Without Fetching
QoraStateBuilder<T> subscribes to query state without triggering a fetch. Use it when another widget or service owns the fetch lifecycle and you only want to display the current state.
// NotificationsScreen (a QoraBuilder) owns the fetch.
// This badge simply mirrors the cached state of the same key.
QoraStateBuilder<List<Notification>>(
queryKey: ['notifications'],
builder: (context, state) {
final count = state.dataOrNull?.length ?? 0;
return Badge(
label: Text('$count'),
isLabelVisible: count > 0,
child: const Icon(Icons.notifications),
);
},
)
QoraStateBuilder Props
| Prop | Type | Description |
|---|---|---|
queryKey | Object (List or QoraKey) | Key to observe. |
builder | Widget Function(BuildContext, QoraState<T>) | Builds the widget tree from the current state. |
client | QoraClient? | Override the client from QoraScope. |
QoraStateBuilder is ideal for badges, counters, or secondary displays that derive their data from a query fetched elsewhere: for example, a cart item count or an unread message badge in an app bar.Next Steps
- Network Aware Guide: Full offline setup with
FetchStatusand offline mutation queue - Network Status:
NetworkStatusBuilderandNetworkStatusIndicatorwidgets - Query States: Full state machine reference
- QoraOptions: Stale time, retry, polling, and
networkModeconfiguration - Optimistic Updates: Instant UI with safe rollback