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 ornull
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 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
-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:
- 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,
);