Infinite Scrolling Pagination in Flutter
Learn how to implement infinite scrolling pagination (also known as lazy loading) in Flutter using the Infinite Scroll Pagination package. By Edson Bueno.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Infinite Scrolling Pagination in Flutter
20 mins
- Getting Started
- Diving Into the Code
- Understanding Pagination
- Automating Pagination
- Creating a Paginated ArticleListView
- Swapping List Widgets
- Engineering Infinite Scrolling Pagination
- Getting to Know the Package
- Instantiating a PagingController
- Fetching Pages
- Using a Paginated ListView
- Building List Items
- Creating a Builder Delegate
- Applying Filters
- Where to Go From Here?
Forget about the gold at the end of the rainbow. Have you ever wondered what’s at the end of your Instagram feed? Has anyone ever scrolled that far? You could find the password to a secret Swiss bank account, the key to immortality or the answer to the meaning of life. Oh, the possibilities.
Today you’ll not only solve that mystery, but will also master this very same infinite scrolling technique. The technique has many names, including infinite scrolling pagination, endless scrolling pagination, auto-pagination, lazy loading pagination, continuous scroll pagination and progressive loading pagination.
By the end of this tutorial, you’ll know:
- What infinite scrolling pagination is
- What its alternatives are
- Why it’s such a big deal
- How to go from a non-paginated app to a paginated one
- How to take that knowledge to any project you work on
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
In case you didn’t know, raywenderlich.com has an app for Android and iOS called (drum roll, please): raywenderlich. The thing is, that one is for videos. Today you’ll be working on a low budget version for articles: readwenderlich!
Unzip the downloaded file and open Android Studio 4.1 or later. You can use Visual Studio Code instead, but you might need to tweak some instructions to follow along.
Click on Open an existing Android Studio project and choose the starter folder from your unzipped download.
Now, download your dependencies by double-clicking pubspec.yaml on the left-side panel (1) and then clicking Pub get (2) at the top of your screen.
Finally, build and run. If everything went OK, you should see something like this:
The starter project only fetches the first 20 items for the filtering and sorting options you set. For example, if you filter by Flutter and sort by Most Popular, you’ll see the 20 most popular Flutter articles.
Diving Into the Code
There are many files in the project, but the ones you need to be familiar with are:
- lib/ui/preferences/list_preferences.dart: A plain Dart class gathering all the filtering and sorting options the user selected.
-
lib/ui/list/article_list_view.dart: A widget that receives
ListPreferences
and uses its information to fetch and display a list of articles. The image below highlights the exact part of the screen controlled by this widget.
The starter project’s state management approach is setState, because that’s the neutral ground for all Flutter developers. But what you’ll learn here applies to any strategy.
Throughout the tutorial, you won’t change ArticleListView
. Instead, you’ll write a substitute for it from scratch.
Rather than a top 20, you want readwenderlich to be a complete catalog of all the articles on raywenderlich.com. For that, you’ll rely on infinite scrolling pagination.
Understanding Pagination
Two guys walk into a bar… just kidding! It’s actually one person, and it’s a woman. You’ll call her the user, while the bar is your app.
The night is just beginning, and you, the bartender, know the user will consume lots of data drinks. What you don’t yet know is how many.
You’re committed to providing the best customer experience, so you start wondering what would be the optimal way of serving your user. You then grab some nearby napkins and start writing out your options:
If you think of drinks like batches of items in a list — the so-called pages — the method above is how non-paginated apps work. It’s no surprise that there aren’t bars that serve like this, but unfortunately, this is how most apps work. Which leads you to another option:
If you’re familiar with the above, don’t worry. It’s not only because you’ve been going a lot to bars, but also because that’s how good old web-like pagination works:
Every time you finish a page, you have to manually ask for another. Good, but still not perfect. It’d be better if you could automate the process.
Automating Pagination
With no luck on your quest for the holy grail of drink service, you’re considering settling for the conventional way… until the customer comes to you with a fantastic idea:
That sounds amazing! But you’re cautious, and you don’t want to make a decision before planning everything out on paper a napkin:
It’s a done deal! Thank goodness; that was your last napkin!
Luckily, someone did science a favor and recorded the experiment:
This process is the same as infinite scrolling pagination: As the user scrolls down your screen, you watch what’s going on and fetch more data before she hits the bottom of the screen, giving her the smooth experience of scrolling an infinite list.
Look, all the cool kids apps are doing it:
So now that you know what you’ll be doing, it’s time to roll up your sleeves.
Creating a Paginated ArticleListView
Start off by creating a new file inside lib/ui/list called paged_article_list_view.dart. Put the following inside the file:
import 'package:flutter/material.dart';
import 'package:readwenderlich/data/repository.dart';
import 'package:readwenderlich/entities/article.dart';
import 'package:readwenderlich/ui/exception_indicators/empty_list_indicator.dart';
import 'package:readwenderlich/ui/exception_indicators/error_indicator.dart';
import 'package:readwenderlich/ui/list/article_list_item.dart';
import 'package:readwenderlich/ui/preferences/list_preferences.dart';
// 1
class PagedArticleListView extends StatefulWidget {
const PagedArticleListView({
// 2
@required this.repository,
// 3
this.listPreferences,
Key key,
}) : assert(repository != null),
super(key: key);
final Repository repository;
final ListPreferences listPreferences;
@override
_PagedArticleListViewState createState() => _PagedArticleListViewState();
}
class _PagedArticleListViewState extends State<PagedArticleListView> {
// 4
ListPreferences get _listPreferences => widget.listPreferences;
// TODO: Instantiate a PagingController.
@override
// 5
Widget build(BuildContext context) => const Placeholder();
}
Here’s what you just did:
- This is the widget that will replace the old non-paginated list onscreen. This is your paginated version of
ArticleListView
. - You’re asking a
Repository
instance from the enclosing widget.Repository
comes with the starter project and helps you communicate with the remote API. - Similarly, you’re now asking a
ListPreferences
instance from the enclosing widget. - You’re creating a computed property to help you access the
ListPreferences
received in the previous step without the need to typewidget.listPreferences
every time. - The
Placeholder
fills the screen with a giant X while you don’t have the paginated list widget figured out yet. You’ll get back to this shortly.
Now you need to be able to visualize what you just created.
Swapping List Widgets
Swap the old ArticleListView
for your fresh PagedArticleListView
. For that, open lib/ui/list/article_list_screen.dart and add an import to your new file at the top:
import 'package:readwenderlich/ui/list/paged_article_list_view.dart';
Since you’re already working with the imports, take the opportunity to do some cleaning by removing the soon-to-be unused ArticleListView
import:
import 'package:readwenderlich/ui/list/article_list_view.dart';
Jump to the build()
and replace the Scaffold
‘s body
property with:
body: PagedArticleListView(
repository: Provider.of<Repository>(context),
listPreferences: _listPreferences,
),
You’re obtaining a Repository
instance from Provider
, your dependency injection system for this project.
That’s it! You can delete the now obsolete lib/ui/list/article_list_view.dart.
Build and run. You should see your Placeholder
in action:
Engineering Infinite Scrolling Pagination
In the whole drink service situation above, you looked at infinite scrolling pagination from a product perspective. Now, put your developer glasses on, divide your goal into pieces and examine what it takes to conquer it:
- Watch the user’s scroll position so that you can fetch other pages in advance.
- Keep track of and transition between every possible status in your list.
- Keep the user posted by displaying indicators for each different status.
- Make a solution that’s reusable in different screens, possibly using other layouts. One example of this is grids. Ideally, this solution should also be portable to different projects with other state management approaches.
Sounds like hard work? It doesn’t have to be. These issues are already addressed by the Infinite Scroll Pagination package, which will be your companion for this article. In the next section, you’ll take a closer look at this package.
Getting to Know the Package
Warm up by opening pubspec.yaml and replacing # TODO: Add infinite_scroll_pagination dependency here.
with infinite_scroll_pagination: ^3.1.0
:
dependencies:
flutter:
sdk: flutter
infinite_scroll_pagination: ^3.1.0
Download your newest dependency by clicking on Pub get in the Flutter commands bar at the top of your screen.
The Infinite Scroll Pagination package makes your job as easy as stealing candy from a baby, shooting fish in a barrel or assembling a three-piece jigsaw puzzle. Speaking of the latter, here’s your first piece:
PagingController
is a controller for paged widgets. It’s responsible for holding the current state of the pagination and request pages from its listeners whenever needed.
If you’ve worked with Flutter’s TextEditingController or ScrollController, for example, you’ll feel at home with PagingController
.
Instantiating a PagingController
Back to lib/ui/list/paged_article_list_view.dart, add an import to the new library at the top of the file:
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
Now, replace // TODO: Instantiate a PagingController.
with:
// 1
final _pagingController = PagingController<int, Article>(
// 2
firstPageKey: 1,
);
@override
void initState() {
// 3
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
Future<void> _fetchPage(int pageKey) async {
// TODO: Implement the function's body.
}
@override
void dispose() {
// 4
_pagingController.dispose();
super.dispose();
}
Here’s a step-by-step explanation of what the code above does:
- When instantiating a
PagingController
, you need to specify two generic types. In your code, they are:-
int
: This is the type your endpoint uses to identify pages. For the raywenderlich.com API, that’s the page number. For other APIs, instead of a page number, that could be aString
token or the number of items to offset. Due to this diversity of pagination strategies, the package calls these identifiers page keys. -
Article
: This is the type that models your list items.
-
- Remember the
int
you specified as a generic type in the previous step? Now you need to provide its initial value by using thefirstPageKey
parameter. For the raywenderlich.com API, page keys start at1
, but other APIs might start at0
. - This is how you register a callback to listen for new page requests.
- Don’t forget to
dispose()
your controller.
Fetching Pages
Your _fetchPage()
implementation doesn’t have much use as it is right now. Fix this by replacing the entire function with:
Future<void> _fetchPage(int pageKey) async {
try {
final newPage = await widget.repository.getArticleListPage(
number: pageKey,
size: 8,
// 1
filteredPlatformIds: _listPreferences?.filteredPlatformIds,
filteredDifficulties: _listPreferences?.filteredDifficulties,
filteredCategoryIds: _listPreferences?.filteredCategoryIds,
sortMethod: _listPreferences?.sortMethod,
);
final previouslyFetchedItemsCount =
// 2
_pagingController.itemList?.length ?? 0;
final isLastPage = newPage.isLastPage(previouslyFetchedItemsCount);
final newItems = newPage.itemList;
if (isLastPage) {
// 3
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
_pagingController.appendPage(newItems, nextPageKey);
}
} catch (error) {
// 4
_pagingController.error = error;
}
}
This is where all the magic happens:
- You’re forwarding the current filtering and sorting options to the repository.
-
itemList
is a property ofPagingController
. It holds all items loaded so far. You’re using the?
conditional property access becauseitemList
initial value isnull
. - Once you have your new items, let the controller know by calling
appendPage()
orappendLastPage()
on it. - If an error occurred, supply it to the controller’s
error
property.
Build and run to make sure you haven’t introduced any errors. Don’t expect any visual or functional changes.
Using a Paginated ListView
Before you move on to the build()
, there’s something you need to know:
The second piece is exactly what its name suggests: a paginated version of a regular ListView
. And as the illustration shows, it’s in there that you’ll fit in your controller.
Still on lib/ui/list/paged_article_list_view.dart, replace the old build()
with:
@override
Widget build(BuildContext context) =>
// 1
RefreshIndicator(
onRefresh: () => Future.sync(
// 2
() => _pagingController.refresh(),
),
// 3
child: PagedListView.separated(
// 4
pagingController: _pagingController,
padding: const EdgeInsets.all(16),
separatorBuilder: (context, index) => const SizedBox(
height: 16,
),
),
);
Here’s what’s going on:
- Wrapping scrollable widgets with Flutter’s
RefreshIndicator
empowers a feature known as swipe to refresh. The user can use this to refresh the list by pulling it down from the top. -
PagingController
definesrefresh()
, a function for refreshing its data. You’re wrapping therefresh()
call in aFuture
, because that’s how theonRefresh
parameter from theRefreshIndicator
expects it. - Like the good old
ListView
you already know,PagedListView
has an alternativeseparated()
constructor for adding separators between your list items. - You’re connecting your puzzle pieces.
Building List Items
After all this, you suspect something might be wrong — after all, what’s going to build your list items?
The good Sherlock Holmes that you are, you investigate by hovering your magnifying glass — also known as a mouse — over PagedListView
:
Well done, detective. You found the missing puzzle piece!
Now, it’s time to put them all together!
Creating a Builder Delegate
For the final touch, fill that same gap in code by specifying this new parameter to your PagedListView
, in the same level as pagingController
, padding
and separatorBuilder
:
builderDelegate: PagedChildBuilderDelegate<Article>(
itemBuilder: (context, article, index) => ArticleListItem(
article: article,
),
firstPageErrorIndicatorBuilder: (context) => ErrorIndicator(
error: _pagingController.error,
onTryAgain: () => _pagingController.refresh(),
),
noItemsFoundIndicatorBuilder: (context) => EmptyListIndicator(),
),
PagedChildBuilderDelegate
is a collection of builders for every widget involved in infinite scrolling pagination.
Although you’ve only specified three parameters in your code, knowing all seven of them might save you in the future:
-
itemBuilder
: This builds your list items. It’s the only required parameter; all others have defaults. -
firstPageErrorIndicatorBuilder
: This builds a widget that informs the user the occurrence of an error fetching the first page. In this scenario, you won’t yet have any list items loaded, so you’ll want this widget to fill the entire space. Make sure you callrefresh()
on your controller if you decide to add a retry button to your indicator — take another look at the previous code snippet and you’ll see it. -
newPageErrorIndicatorBuilder
: This also indicates an error, but for subsequent page requests. As you already have some items loaded, the widget you build here appears at the bottom of your list. If you add a retry button to your indicator, make sure you callretryLastRequest()
on your controller when the user taps your button. -
firstPageProgressIndicatorBuilder
: This builds the widget that’ll show while loading the first page. You won’t yet have any items loaded, so you’ll want this widget to fill the entire space. -
newPageProgressIndicatorBuilder
: This is similar to the previous builder, but it shows while loading subsequent pages. As you already have some items loaded, the widget you build here appears at the bottom of your list. -
noItemsFoundIndicatorBuilder
: What if your API successfully returns an empty list? Technically that’s not an error, and it’s usually associated with too many filter options selected. The widget you build here covers this “zero items” scenario. -
noMoreItemsIndicatorBuilder
: Here it is, the gold at the end of the rainbow! This is where you optionally build a widget to display when the user finally reaches the end of your list. Mystery solved!
Build and run. Try turning off your device’s connection and using swipe to refresh as shown in the GIF below. This will help you test both RefreshIndicator
and the custom error indicator widget.
Hmm, there’s a bug in your app. Apparently that wasn’t the final touch after all. Try figuring out what’s wrong. If you’re stuck, click the Reveal button below to find out what it is.
[spoiler title=”Filters Bug”]
Applying filters doesn’t seem to be taking any effect.
[/spoiler]
Applying Filters
You know what they say: Be suspicious if your code works on the first try. Well, yours didn’t, so you’re probably doing something right!
Here’s what’s causing the bug: When the user applies filters, ArticleListScreen
rebuilds your PagedArticleListView
with a brand-new ListPreferences
, but no one warned your PagingController
about the change.
The place for solving this is lib/ui/list/paged_article_list_view.dart. Inside _PagedArticleListViewState
, add the following function override:
@override
void didUpdateWidget(PagedArticleListView oldWidget) {
if (oldWidget.listPreferences != widget.listPreferences) {
_pagingController.refresh();
}
super.didUpdateWidget(oldWidget);
}
This is what the code above is saying: Whenever the widget associated with this State
subclass updates, if the listPreferences
also changed, refresh the _pagingController
by calling refresh()
on it.
Mission accomplished. Time to build and run readwenderlich for the last time and allow the wonder of infinite scrolling pagination to amaze you. You did it!
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
Now for the coolest part: The Infinite Scroll Pagination package comes with replacement parts for your middle piece. Choose whichever suits your next layout the best.
You can find more details on these extra pieces in the package’s cookbook.
Here’s a suggestion: The endpoint you used to fetch your articles accepts search queries. How about using that to add search support to readwenderlich?
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!