How to implement infinite scrolling in flutter
Aza Raskin, the OG creator of infinite scrolling, gave us modern-age social media. It’s used in TikTok, reels, Facebook, Instagram, etc.
The basic idea of infinite scrolling boils down to 3 parts
- Initial loading of elements — These are the first loaded elements of the scrolling list. For this example, let’s call for 10 such elements in the first load.
- Scroll Listeners — We continuously listen for user scrolls and identify how many scrolls are left till the end of the list.
- Appending the list — Once we reach 70% of the initial scroll length we call for the next 10 elements and add them to the list.
Now as we have jotted down the idea for an infinite scrolling list let's implement it.
Pre-Requisite
I have used Provider for state management. This is a simple example but if you are planning to add this feature to your existing code base then let me know in the comments and I will share relevant changes based on your preferred state management library.
AppState — Provider Logic
I have initialized pageNumber and pageSize so that we can send this directly to the API calls and get the results accordingly.
class AppState with ChangeNotifier {
final int pageSize = 20;
int pageNumber = 1;
List<int> intList = [];
bool isLoading = false;
bool showFab = false;
void _addToList(List<int> list) {
intList.addAll(list);
}
void _notifyListeners() {
notifyListeners();
}
void _isLoading(bool loadingState) {
isLoading = loadingState;
}
void _appendPageNumber() {
pageNumber = pageNumber + 1;
}
Future<bool> loadInitialData() async {
intList.clear();
final list = await API.getCount(pageSize: pageSize, pageNumber: pageNumber);
intList = list;
_appendPageNumber();
return true;
}
void appendList() async {
_isLoading(true);
_notifyListeners();
final list = await API.getCount(pageSize: pageSize, pageNumber: pageNumber);
_appendPageNumber();
_addToList(list);
_isLoading(false);
_notifyListeners();
}
void showFabButton(bool state) {
showFab = state;
_notifyListeners();
}
}
Initially, when we load the application we have to load the initial data list. That is why I have created a method called load initial data.
After that whenever we need to call the next data we are simply calling the appendList() method to call for the next data and incrementing pageNumber with each call.
We are also managing the loading state as well when our app is busy getting data we need to show a loader to the user that is managed by the _isLoading() function.
UI — Application UI
This is where the magic happens. We have initialized a ScrollController and are listening for changes in the offset of the scroll position. We have created a final variable called maxVerticalScrollBound which is the max scroll bounds that our scroller allows.
Then we have defined the triggerPoint that is set to 70% of maxVerticalScrollBound. Then we check if the currentScrollOffset > triggerPoint && appState.isLoading == false then we are getting the new data.
class InfiniteScrollList extends StatefulWidget {
const InfiniteScrollList({super.key});
@override
State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}
class _InfiniteScrollListState extends State<InfiniteScrollList> {
final ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
_controller.addListener(scrollListener);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
void scrollListener() {
final appState = Provider.of<AppState>(context, listen: false);
final maxVerticalScrollBound = _controller.position.maxScrollExtent;
final triggerPoint = maxVerticalScrollBound * 70 / 100;
final currentScrollOffset = _controller.offset;
if (currentScrollOffset > 0.0 && !appState.showFab) {
appState.showFabButton(true);
}
if (currentScrollOffset == 0.0 && appState.showFab) {
appState.showFabButton(false);
}
if (currentScrollOffset > triggerPoint && !appState.isLoading) {
log('Triggered');
appState.appendList();
}
}
bool isControllerAtTop() {
return _controller.offset > 0.0;
}
void scrollToTop() => _controller.animateTo(0.0,
duration: const Duration(milliseconds: 300), curve: Curves.bounceInOut);
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
child: Consumer<AppState>(
builder: (context, value, child) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Infinite Scrolling List'),
centerTitle: true,
),
floatingActionButton: value.showFab
? FloatingActionButton(
onPressed: scrollToTop,
child: const Icon(Icons.arrow_circle_up_rounded),
)
: null,
body: ListView.builder(
controller: _controller,
shrinkWrap: true,
itemCount: value.intList.length + (value.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index == value.intList.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(child: CircularProgressIndicator()),
);
}
final element = value.intList[index];
return Padding(
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3.0,
spreadRadius: 3.0,
),
],
),
child: ListTile(
title: Text("Element No $element"),
subtitle: Text("Is Even => ${element.isEven}"),
),
),
);
},
),
);
},
),
);
}
}
We have used ListView.builder() as It allows us to lazy load the elements and is not expensive to render. And if the app state is loading then we are adding another widget to the listViewBuilder which is the CircularProgressIndicator
API — Mock API Call
Here we have defined an API class which has a mock API method called getCount which takes 2 parameters pageSize and pageNumber and returns the next List of int values accordingly.
class API {
/// Mock API which takes `pageSize` and `pageNumber` to return list of numbers
static Future<List<int>> getCount(
{required int pageSize, required int pageNumber}) async {
log("=======API Called======");
log("=======PageNumber $pageNumber======");
List<int> returnList = [];
for (int i = ((pageNumber * pageSize) - pageSize);
i < pageNumber * pageSize;
i++) {
returnList.add(i + 1);
}
//Change this to make fast or slow
await Future.delayed(const Duration(seconds: 2));
return returnList;
}
}
If you want to check the whole code you can clone the repository and run the code yourself to check how API calls are triggered.
abhisheksingh-dev/infinite_scrolling_example_flutter: Implementation of Infinite Scroll (github.com)