Skip to main content

Infinite Queries

Rendering lists that can additively "load more" data onto an existing set of data or "infinite scroll" is also a very common UI pattern. Fl Query supports a useful version of Query called InfiniteQuery for querying these types of lists.

Create an InfiniteQuery

The InfiniteQueryBuilder/useInfiniteQuery is used to create InfiniteQueries. It's almost same as QueryBuilder and useQuery

Here's how to create one:

InfiniteQueryBuilder<PagedProducts, ClientException, int>(
"products",
(page) => api.getProductsPaginated(page),
nextPage: (lastPage, lastPageData) {
/// returning [null] will set [hasNextPage] to [false]
if (lastPageData.products.length < 10) return null;
return lastPage + 1;
},
initialPage: 0,
builder: /*...*/
);

InfiniteQuery has some required parameters:

  • key(unnamed)
  • queryFn(unnamed)
  • nextPage - A function that returns the next page number or null if there are no more pages.
  • initialPage - The initial page to start from.

All the Type parameters of both InfiniteQueryBuilder and useInfiniteQuery might seem overwhelming but using these makes your code more type safe and easier to understand. So the type parameters are:

  • <DataType> - The type of data returned by the queryFn
  • <ErrorType> - The type of error returned by the queryFn
  • <PageType> - The type of page
note

Make sure to return null for nextPage to indicate there's no more pages to load.

InfiniteQuery

An InfiniteQuery will passed/returned by the InfiniteQueryBuilder/useInfiniteQuery which can used to manipulate the InfiniteQuery

States

Just like Query an InfiniteQuery has 2 groups of states: 1. Progressive States 2. Data availability States

  • Progressive States
    • isLoadingNextPage - true if the next page is currently loading.
    • isRefreshingPage - true if the current page is currently refreshing.
    • isInactive - true if the query is not fetching and has no errors and has no listeners
  • Data availability states
    • hasNextPage - true if there is a next page to fetch.
    • hasPages - true if there are pages available.
    • hasErrors - true if there are errors in any pages.
    • hasPageData - true if data is available for the current page.
    • hasPageError - true if there's an error in the current page.

Here's an example of how these states can be used to render a paginated list:

/// Inside the [builder] of previous example
final products = query.pages.map((e) => e.products).expand((e) => e);

return ListView(
children: [
for (final product in products)
ListTile(
title: Text(product.title),
subtitle: Text(product.description),
leading: Image.network(product.thumbnail),
),
if (query.hasNextPage && query.isLoadingNextPage)
ElevatedButton(
onPressed: () => query.fetchNext(),
child: Text("Load More"),
)
else if (query.hasNextPage && !query.isLoadingNextPage)
ElevatedButton(
onPressed: null,
child: const CircularProgressIndicator(),
),
if (query.hasErrors)
...query.errors.map((e) => Text(e.message)).toList(),
],
);

Fetching next page

InfiniteQuery has hasNextPage that must be used to check if there's any pages left to fetch. The fetchNext can be used to fetch the next page.

if (query.hasNextPage){
await query.fetchNext();
}

Also InfiniteQuery.isLoadingNextPage can be used to show a loading indicator while the next page is loading.

ListView(
children: [
// ...
if (query.hasNextPage && query.isLoadingNextPage)
CircularProgressIndicator(),
],
)

Refreshing

InfiniteQuery uses pages to store data and each individual page are fetched/refreshed in a sequeunce. InfiniteQuery provides two methods InfiniteQuery.refresh and InfiniteQuery.refreshAll to refresh once page or all pages at once.

Refresh a current page:

await query.refresh();

Passing no page argument will refresh the current page by default.

Refresh a specific page:

await query.refresh(2);

Refresh all pages:

await query.refreshAll();
note

Refreshing all pages can be really expensive and should be done with caution.

If you need refresh specific pages or to refresh some segments, you can just combine refresh with Future.wait or just a plain for loop.

await Future.wait(
[1, 2, 3].map((e) => query.refresh(e)),
);

Set page data manually

InfiniteQuery provides a method InfiniteQuery.setPageData to set page data manually. This can be useful if you want to set data after a mutation for Optmisitc Updates

query.setPageData(0, [...query.pages[0], newProduct])

You can use InfiniteQuery.pages.map to set page data for all pages.

note

If the specified page doesn't exist, setPageData will create a new page and add the data to it.

Dynamic Key

Just like Query with dart's String interpolation, you can pass dynamic keys to the InfiniteQuery. This will create new instance of InfiniteQuery for every dynamically generated unique key

InfiniteQueryBuilder<PagedProducts, ClientException, int>(
"category/$categoryId/products",
(page) => api.getProductsPaginated(page, categoryId),
nextPage: (lastPage, lastPageData) {
/// returning [null] will set [hasNextPage] to [false]
if (lastPageData.products.length < 10) return null;
return lastPage + 1;
},
initialPage: 0,
builder: /*...*/
);

Lazy InfiniteQuery

Just like Query by default InfiniteQueries are executed immediately after they are mounted. But you can also make them lazy by passing enabled: false to the InfiniteQueryBuilder or useInfiniteQuery Until InfiniteQuery.fetch or InfiniteQuery.refresh is called, anything won't be fetched

InfiniteQueryBuilder<PagedProducts, ClientException, int>(
"lazy-products",
(page) => api.getProductsPaginated(page),
nextPage: (lastPage, lastPageData) {
/// returning [null] will set [hasNextPage] to [false]
if (lastPageData.products.length < 10) return null;
return lastPage + 1;
},
initialPage: 0,
enabled: false,
builder: /*...*/
);