Almost every REST API requires you to handle pagination. When querying an API for some resource instead of delivering all of the results, which could be time-consuming. Pagination breaks down a list into equal smaller pieces, loaded one at a time.
In this tutorial, we cover how to implement Pagination with ListView and handle HTTP request for new data. The common application feature is to load more data automatically as the user scrolls through the data.

Create Stateful Widgets
First, We will build Stateful Widget for ListView. It helps to add new data to the ListView.The state is information that can be read synchronously when the widget is built and might change during a lifetime of a widget.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String nextPage = "https://swapi.co/api/people";
ScrollController _scrollController = new ScrollController();
bool isLoading = false;
List names = new List();
....
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Pagination"),
),
body: Container(
child: _buildList(),
),
resizeToAvoidBottomPadding: false,
);
}
}
The setState is notified when such state changes, using the State.setState method.
Fetch Data using dio
Here, we will look at fetching data from an API, as a real-time scenario. This will give us a better understanding of Flutter Pagination. First, we’ll look at what API to use. Next, we will set up a networking library that will help us perform these API calls. Here we use SWAPI API. It returns people’s names with the next page URL.
Add dependency
dependencies:
....
dio: 2.0.22 # check for latest version.
The dio is Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.
What we’re going to do. Loading the next set of data (next page), by specifying its next page URL.
final dio = new Dio();
void _getMoreData() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
final response = await dio.get(nextPage);
List tempList = new List();
nextPage = response.data['next'];
for (int i = 0; i < response.data['results'].length; i++) {
tempList.add(response.data['results'][i]);
}
setState(() {
isLoading = false;
names.addAll(tempList);
});
}
}
Inside the _getMoreData method, load additional items into the List by sending out a network request.
Loading More Data
Let’s see what that scrollController looks like. The responsibility of this listener is to load more items only if some conditions are satisfied. If you aren’t currently loading items and the last page hasn’t been reached then it checks against the current position that is in view to decide whether or not to load more items.
@override
void initState() {
this._getMoreData();
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
ListView has support for binding to the scrollController events which are triggered whenever a user scrolls through the collection.
_getMoreData() actually retrieve the new data.Now as you scroll, items will be automatically filling in List because the onLoadMore method will be triggered once the user
Displaying Progress with ListView
To display the last row as a
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
Widget _buildList() {
return ListView.builder(
//+1 for progressbar
itemCount: names.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == names.length) {
return _buildProgressIndicator();
} else {
return new ListTile(
title: Text((names[index]['name'])),
onTap: () {
print(names[index]);
},
);
}
},
controller: _scrollController,
);
}
Complete example
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String nextPage = "https://swapi.co/api/people";
ScrollController _scrollController = new ScrollController();
bool isLoading = false;
List names = new List();
final dio = new Dio();
void _getMoreData() async {
if (!isLoading) {
setState(() {
isLoading = true;
});
final response = await dio.get(nextPage);
List tempList = new List();
nextPage = response.data['next'];
for (int i = 0; i < response.data['results'].length; i++) {
tempList.add(response.data['results'][i]);
}
setState(() {
isLoading = false;
names.addAll(tempList);
});
}
}
@override
void initState() {
this._getMoreData();
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
_getMoreData();
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
Widget _buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 00,
child: new CircularProgressIndicator(),
),
),
);
}
Widget _buildList() {
return ListView.builder(
//+1 for progressbar
itemCount: names.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == names.length) {
return _buildProgressIndicator();
} else {
return new ListTile(
title: Text((names[index]['name'])),
onTap: () {
print(names[index]);
},
);
}
},
controller: _scrollController,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Pagination"),
),
body: Container(
child: _buildList(),
),
resizeToAvoidBottomPadding: false,
);
}
}