Skip to content

Pagination

Implement pagination with Fasq using query keys that include page parameters.

Use page numbers in your query keys to enable pagination:

class PostsScreen extends StatefulWidget {
@override
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
int currentPage = 1;
final int pageSize = 10;
@override
Widget build(BuildContext context) {
return QueryBuilder<List<Post>>(
queryKey: 'posts:page:$currentPage:size:$pageSize',
queryFn: () => api.fetchPosts(page: currentPage, size: pageSize),
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
if (state.hasError) return Text('Error: ${state.error}');
if (state.hasData) {
return Column(
children: [
Expanded(
child: ListView.builder(
itemCount: state.data!.length,
itemBuilder: (context, index) => PostTile(state.data![index]),
),
),
PaginationControls(
currentPage: currentPage,
onPageChanged: (page) => setState(() => currentPage = page),
),
],
);
}
return SizedBox();
},
);
}
}

Create reusable pagination controls:

class PaginationControls extends StatelessWidget {
final int currentPage;
final Function(int) onPageChanged;
final bool hasNextPage;
final bool hasPreviousPage;
const PaginationControls({
required this.currentPage,
required this.onPageChanged,
this.hasNextPage = true,
this.hasPreviousPage = true,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: hasPreviousPage ? () => onPageChanged(currentPage - 1) : null,
icon: Icon(Icons.chevron_left),
),
Text('Page $currentPage'),
IconButton(
onPressed: hasNextPage ? () => onPageChanged(currentPage + 1) : null,
icon: Icon(Icons.chevron_right),
),
],
);
}
}

Prefetch the next page for better user experience:

class PostsScreen extends StatefulWidget {
@override
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
int currentPage = 1;
void _prefetchNextPage() {
final nextPage = currentPage + 1;
QueryClient().prefetchQuery(
'posts:page:$nextPage:size:10',
() => api.fetchPosts(page: nextPage, size: 10),
);
}
@override
Widget build(BuildContext context) {
return QueryBuilder<List<Post>>(
queryKey: 'posts:page:$currentPage:size:10',
queryFn: () => api.fetchPosts(page: currentPage, size: 10),
builder: (context, state) {
if (state.hasData) {
// Prefetch next page when current page loads
WidgetsBinding.instance.addPostFrameCallback((_) {
_prefetchNextPage();
});
}
return buildUI(state);
},
);
}
}

With the hooks adapter, pagination becomes more declarative:

class PostsScreen extends HookWidget {
@override
Widget build(BuildContext context) {
final currentPage = useState(1);
final pageSize = 10;
final postsState = useQuery(
'posts:page:${currentPage.value}:size:$pageSize',
() => api.fetchPosts(page: currentPage.value, size: pageSize),
);
return Column(
children: [
if (postsState.isLoading) CircularProgressIndicator(),
if (postsState.hasData) PostsList(postsState.data!),
PaginationControls(
currentPage: currentPage.value,
onPageChanged: (page) => currentPage.value = page,
),
],
);
}
}

With Riverpod, create a family provider for pagination:

final postsProvider = queryProvider.family<List<Post>, int>(
(page) => 'posts:page:$page:size:10',
(page) => api.fetchPosts(page: page, size: 10),
);
class PostsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentPage = useState(1);
final postsState = ref.watch(postsProvider(currentPage.value));
return Column(
children: [
if (postsState.isLoading) CircularProgressIndicator(),
if (postsState.hasData) PostsList(postsState.data!),
PaginationControls(
currentPage: currentPage.value,
onPageChanged: (page) => currentPage.value = page,
),
],
);
}
}

Manage cache for paginated data:

class PostsScreen extends StatefulWidget {
@override
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends State<PostsScreen> {
int currentPage = 1;
void _clearCache() {
// Clear all posts cache
QueryClient().invalidateQueriesWithPrefix('posts:');
}
void _prefetchPage(int page) {
QueryClient().prefetchQuery(
'posts:page:$page:size:10',
() => api.fetchPosts(page: page, size: 10),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(
onPressed: _clearCache,
child: Text('Clear Cache'),
),
QueryBuilder<List<Post>>(
queryKey: 'posts:page:$currentPage:size:10',
queryFn: () => api.fetchPosts(page: currentPage, size: 10),
builder: (context, state) => buildUI(state),
),
],
);
}
}
  1. Use descriptive query keys - Include page and size parameters
  2. Prefetch adjacent pages - Improve perceived performance
  3. Configure cache time - Keep frequently accessed pages cached
  4. Invalidate on data changes - Clear cache when data is updated
  5. Use appropriate page sizes - Balance between requests and memory