Infinite Query
InfiniteData<TData, TPageParam>
Immutable container holding all fetched pages and their corresponding page parameters.
TData is the type of a single page (e.g. List<Post>). TPageParam is the type of the page parameter (e.g. int, String?).
Both pages and pageParams are always the same length: pages[i] was fetched using pageParams[i].
Constructor
const InfiniteData({
required List<TData> pages,
required List<TPageParam> pageParams,
})
Asserts that pages.length == pageParams.length.
Properties
| Property | Type | Description |
|---|---|---|
pages | List<TData> | All fetched pages in order: first page at index 0. |
pageParams | List<TPageParam> | Parameters used to fetch each page (pageParams[i] → pages[i]). |
pageCount | int | Total number of pages currently loaded. |
isEmpty | bool | true when no pages have been loaded. |
isNotEmpty | bool | true when at least one page has been loaded. |
Methods
flatten
List<TItem> flatten<TItem>(Iterable<TItem> Function(TData page) selector)
Flattens all pages into a single list: the canonical way to convert paginated data for ListView.builder.
// TData == List<Post>
final posts = data.flatten((page) => page);
// TData is a response object with a nested list
final posts = data.flatten((page) => page.items);
append
InfiniteData<TData, TPageParam> append(TData page, TPageParam param)
Returns a new InfiniteData with page and param added at the end. Used internally by fetchNextPage.
prepend
InfiniteData<TData, TPageParam> prepend(TData page, TPageParam param)
Returns a new InfiniteData with page and param added at the front. Used internally by fetchPreviousPage.
dropFirst
InfiniteData<TData, TPageParam> dropFirst()
Returns a new InfiniteData without its first page. Called during windowed paging when maxPages is exceeded after a fetchNextPage.
dropLast
InfiniteData<TData, TPageParam> dropLast()
Returns a new InfiniteData without its last page. Called during windowed paging when maxPages is exceeded after a fetchPreviousPage.
InfiniteQueryState<TData, TPageParam>
Sealed class state machine for an infinite query. Pattern-match exhaustively with switch:
switch (state) {
case InfiniteInitial() || InfiniteLoading():
return const CircularProgressIndicator();
case InfiniteSuccess(:final data, :final hasNextPage, :final isFetchingNextPage):
return PostFeed(
items: data.flatten((page) => page),
hasMore: hasNextPage,
loadingMore: isFetchingNextPage,
);
case InfiniteFailure(:final error, :final previousData):
if (previousData != null) {
return Column(children: [
PostFeed(items: previousData.flatten((p) => p)),
ErrorBanner(error),
]);
}
return ErrorScreen(error);
}
InfiniteInitial
final class InfiniteInitial<TData, TPageParam> extends InfiniteQueryState<TData, TPageParam>
The query has not been executed yet. This is the default state when no fetch has been triggered.
InfiniteLoading
final class InfiniteLoading<TData, TPageParam> extends InfiniteQueryState<TData, TPageParam>
The initial page fetch is in progress. No pages have been loaded yet.
After the first page is loaded, the state never returns to InfiniteLoading. Subsequent page fetches signal progress via isFetchingNextPage / isFetchingPreviousPage on InfiniteSuccess, keeping existing pages visible.
InfiniteSuccess
final class InfiniteSuccess<TData, TPageParam> extends InfiniteQueryState<TData, TPageParam>
All currently loaded pages, with pagination status flags.
Properties
| Property | Type | Description |
|---|---|---|
data | InfiniteData<TData, TPageParam> | All loaded pages and their page parameters. |
hasNextPage | bool | true when getNextPageParam returns non-null for the last page. |
hasPreviousPage | bool | true when getPreviousPageParam returns non-null for the first page. Always false if no getPreviousPageParam. |
updatedAt | DateTime | When this state was last successfully updated. |
isFetchingNextPage | bool | true while a next-page fetch is in progress. Show a bottom loading indicator; data is still valid. |
isFetchingPreviousPage | bool | true while a previous-page fetch is in progress. Show a top loading indicator; data is still valid. |
copyWith
InfiniteSuccess<TData, TPageParam> copyWith({
InfiniteData<TData, TPageParam>? data,
bool? isFetchingNextPage,
bool? isFetchingPreviousPage,
bool? hasNextPage,
bool? hasPreviousPage,
DateTime? updatedAt,
})
Returns a copy with the specified fields overridden. Unspecified fields retain their current values.
InfiniteFailure
final class InfiniteFailure<TData, TPageParam> extends InfiniteQueryState<TData, TPageParam>
A fetch failure, with optional previousData for graceful degradation.
Properties
| Property | Type | Description |
|---|---|---|
error | Object | The error thrown by the fetch function (after all retries). |
stackTrace | StackTrace? | Optional stack trace. |
previousData | InfiniteData<TData, TPageParam>? | Last successfully loaded pages, if any. Non-null when failure occurs after pages have already been loaded. |
When previousData is non-null, show existing content alongside an error indicator rather than replacing the entire view.
InfiniteQueryOptions<TData, TPageParam>
Configuration for an infinite query. Combines standard QoraOptions with pagination-specific parameters.
Constructor
const InfiniteQueryOptions({
required TPageParam initialPageParam,
required TPageParam? Function(TData lastPage, List<TData> allPages) getNextPageParam,
TPageParam? Function(TData firstPage, List<TData> allPages)? getPreviousPageParam,
int? maxPages,
QoraOptions baseOptions = const QoraOptions(),
})
Asserts maxPages == null || maxPages >= 1.
Properties
| Property | Type | Required | Description |
|---|---|---|---|
initialPageParam | TPageParam | Yes | Parameter for the very first page (e.g. 1, 0, null, or ''). |
getNextPageParam | TPageParam? Function(TData lastPage, List<TData> allPages) | Yes | Returns the next page's parameter, or null when no forward pages remain (sets hasNextPage to false). |
getPreviousPageParam | TPageParam? Function(TData firstPage, List<TData> allPages)? | : | Returns the previous page's parameter. Only needed for bi-directional scroll. When null, hasPreviousPage is always false. |
maxPages | int? | : | Maximum pages kept in memory. When exceeded, the oldest page is dropped on fetchNextPage (or newest on fetchPreviousPage). Must be >= 1. |
baseOptions | QoraOptions | : | Standard retry, stale time, cache time, and network mode options applied to every page fetch. Merged on top of QoraClientConfig.defaultOptions. |
InfiniteQueryObserver<TData, TPageParam>
The central pagination engine. Manages the full lifecycle of a single infinite query: fetching pages, emitting state transitions, and respecting concurrency guards.
Typically created inside a Flutter widget's State.initState and disposed in State.dispose. For a ready-made Flutter binding, use InfiniteQueryBuilder instead.
Multiple observers sharing the same key on the same client share the same underlying cache entry, so all listeners receive the same state transitions in sync.
Constructor
InfiniteQueryObserver({
required QoraClient client,
required Object key,
required InfiniteQueryFunction<TData, TPageParam> fetcher,
required InfiniteQueryOptions<TData, TPageParam> options,
})
Properties
| Property | Type | Description |
|---|---|---|
stream | Stream<InfiniteQueryState<TData, TPageParam>> | Emits the current state immediately, then all future transitions. |
state | InfiniteQueryState<TData, TPageParam> | Synchronous snapshot of the current state. |
Methods
fetch
Future<void> fetch()
Fetch the first page. No-op when:
- Data is already loaded (state is not
InfiniteInitial). - An initial fetch is already in progress.
Transitions: InfiniteInitial → InfiniteLoading → InfiniteSuccess | InfiniteFailure.
fetchNextPage
Future<void> fetchNextPage()
Fetch the next page and append it. No-op when:
- No data has been loaded yet (call
fetchfirst). hasNextPageisfalse.- A next-page or previous-page fetch is already in progress.
While fetching, sets isFetchingNextPage: true on the current InfiniteSuccess: existing pages stay visible. On failure, transitions to InfiniteFailure with previousData populated.
fetchPreviousPage
Future<void> fetchPreviousPage()
Fetch the previous page and prepend it. No-op when:
getPreviousPageParamis not configured.hasPreviousPageisfalse.- A fetch is already in progress.
While fetching, sets isFetchingPreviousPage: true. On failure, transitions to InfiniteFailure with previousData.
refetch
Future<void> refetch()
Re-fetch all currently loaded pages in sequence, preserving the page count. Uses the original pageParams stored in InfiniteData. Useful after client.invalidateInfiniteQuery(key) or pull-to-refresh.
- If no data is loaded, delegates to
fetch(). - Signals progress via
isFetchingNextPage: trueon the currentInfiniteSuccess. - On failure, transitions to
InfiniteFailurewithpreviousData.
dispose
void dispose()
Releases this observer. After disposal, all further method calls are silently ignored. The underlying cache entry is not removed: other observers sharing the same key continue operating normally.
Concurrency Guarantees
All guards (_isFetchingInitial, _isFetchingNext, _isFetchingPrevious, _isRefetching) are set as the first synchronous statement in their method: before any await. This prevents race conditions from rapid scroll events or UI interactions even before the first network round-trip begins.
InfiniteQueryBuilder<TData, TPageParam>
Flutter widget that binds an infinite query to the widget tree. See the InfiniteQueryBuilder guide for usage examples.
Constructor
const InfiniteQueryBuilder({
Key? key,
required Object queryKey,
required InfiniteQueryFunction<TData, TPageParam> fetcher,
required InfiniteQueryOptions<TData, TPageParam> options,
required Widget Function(
BuildContext context,
InfiniteQueryState<TData, TPageParam> state,
InfiniteQueryController<TData, TPageParam> controller,
) builder,
QoraClient? client,
bool enabled = true,
})
Props
| Prop | Type | Default | Description |
|---|---|---|---|
queryKey | Object (List or QoraKey) | required | Cache key. Changing it disposes the current observer and starts fresh from initialPageParam. |
fetcher | InfiniteQueryFunction<TData, TPageParam> | required | Future<TData> Function(TPageParam pageParam): fetches a single page. |
options | InfiniteQueryOptions<TData, TPageParam> | required | Pagination configuration. |
builder | Widget Function(BuildContext, InfiniteQueryState, InfiniteQueryController) | required | Builds the widget tree from state and controller. |
client | QoraClient? | null | Override the QoraClient from QoraScope. Useful for testing. |
enabled | bool | true | When false, the first-page fetch is suppressed. The widget still observes the current cached state. |
InfiniteQueryController<TData, TPageParam>
A stable interface exposing pagination actions to the InfiniteQueryBuilder.builder callback. The controller reference is stable as long as queryKey and client remain unchanged.
Methods
| Method | Signature | Description |
|---|---|---|
fetchNextPage() | Future<void> | Fetch and append the next page. No-op if hasNextPage is false or a fetch is already in progress. |
fetchPreviousPage() | Future<void> | Fetch and prepend the previous page. Requires getPreviousPageParam to be configured. |
refetch() | Future<void> | Re-fetch all loaded pages in sequence. Useful for pull-to-refresh (onRefresh: controller.refetch). |
InfiniteQueryFunction<TData, TPageParam>
typedef InfiniteQueryFunction<TData, TPageParam> = Future<TData> Function(
TPageParam pageParam,
);
The type of the fetcher parameter on InfiniteQueryObserver and InfiniteQueryBuilder. Receives the page parameter computed by getNextPageParam / getPreviousPageParam and returns a Future resolving to a single page of data.
QoraClient: Infinite Query Methods
watchInfiniteState
Stream<InfiniteQueryState<TData, TPageParam>> watchInfiniteState<TData, TPageParam>(Object key)
Returns a broadcast stream that immediately emits the current InfiniteQueryState for key, then emits all future transitions. Subscribing prevents the cache entry from being garbage-collected.
getInfiniteQueryState
InfiniteQueryState<TData, TPageParam> getInfiniteQueryState<TData, TPageParam>(Object key)
Synchronous snapshot of the current state. Returns InfiniteInitial() if no entry exists in cache.
getInfiniteQueryData
InfiniteData<TData, TPageParam>? getInfiniteQueryData<TData, TPageParam>(Object key)
Returns the InfiniteData from the current InfiniteSuccess state, or null if the state is not InfiniteSuccess.
final snapshot = client.getInfiniteQueryData<List<Post>, int>(['posts']);
setInfiniteQueryData
void setInfiniteQueryData<TData, TPageParam>(
Object key,
InfiniteData<TData, TPageParam> data, {
bool? hasNextPage,
bool? hasPreviousPage,
})
Inject InfiniteData directly into the cache, pushing an InfiniteSuccess state to all active subscribers. Use this for optimistic updates.
// Optimistically prepend a new post to page 1
final snapshot = client.getInfiniteQueryData<List<Post>, int>(['posts']);
if (snapshot != null) {
client.setInfiniteQueryData(
['posts'],
InfiniteData(
pages: [
[Post.optimistic(title), ...snapshot.pages.first],
...snapshot.pages.skip(1),
],
pageParams: snapshot.pageParams,
),
);
}
try {
await api.createPost(title);
client.invalidateInfiniteQuery(['posts']);
} catch (_) {
if (snapshot != null) client.setInfiniteQueryData(['posts'], snapshot);
}
updateInfiniteQueryState
void updateInfiniteQueryState<TData, TPageParam>(
Object key,
InfiniteQueryState<TData, TPageParam> state,
)
The raw write path for any InfiniteQueryState. Used internally by InfiniteQueryObserver. Prefer setInfiniteQueryData for data-only updates.
invalidateInfiniteQuery
void invalidateInfiniteQuery(Object key)
Reset the cache entry for key to InfiniteInitial. Active InfiniteQueryBuilder instances detect the reset and automatically re-fetch the first page, so the UI never stays blank.
await api.createPost(title);
client.invalidateInfiniteQuery(['posts']);
Related
- Infinite Queries Guide: State machine, pagination strategies, bi-directional scroll, windowed paging
- InfiniteQueryBuilder: Flutter widget reference with patterns and examples
- QoraClient API: Full client reference for standard (non-paginated) queries
- QoraOptions: Retry, stale time, cache time, and network mode