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:
- Vanilla
- Flutter Hooks
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: /*...*/
);
final query = useInfiniteQuery<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,
);
InfiniteQuery has some required parameters:
key(unnamed)queryFn(unnamed)nextPage- A function that returns the next page number ornullif 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 thequeryFn<ErrorType>- The type of error returned by thequeryFn<PageType>- The type of page
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-trueif the next page is currently loading.isRefreshingPage-trueif the current page is currently refreshing.isInactive-trueif the query is not fetching and has no errors and has no listeners
- Data availability states
hasNextPage-trueif there is a next page to fetch.hasPages-trueif there are pages available.hasErrors-trueif there are errors in any pages.hasPageData-trueif data is available for the current page.hasPageError-trueif there's an error in the current page.
Here's an example of how these states can be used to render a paginated list:
- Vanilla
- Flutter Hooks
/// 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(),
],
);
/// Using the [query] from previous example
final products = useMemoized(
() => query.pages.map((e) => e.products).expand((e) => e),
[query.pages],
);
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();
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.
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
- Vanilla
- Flutter Hooks
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: /*...*/
);
final query = useInfiniteQuery<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,
);
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
- Vanilla
- Flutter Hooks
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: /*...*/
);
final query = useInfiniteQuery<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,
);