I am new to Flutter. I want to do the pagination with a REST API. My question is how to add an infinite scroll and then load the data to the next page. How can I load to "https://MY_API_URL?page=2", page 3 and so on?
6 Answers 6
Edit
change sendPagesDataRequest to the following should work
if json string you gave me is correct
Future<PagesData> sendPagesDataRequest(int page) async {
print('page ${page}');
try {
/*String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');*/
String url = Uri.encodeFull("https://MY_API_URL?page=$page");
http.Response response = await http.get(url);
print('body ${response.body}');
/*String responseString = '''
{"current_page": 1,
"data": [
{ "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"},
{ "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"}
],
"first_page_url": "https:/API_URL?page=1",
"from": 1,
"last_page": 30,
"last_page_url": "https:/API_URLpage=30",
"next_page_url": "https:/API_URL?page=2"
}
''';*/
PagesData pagesData = pagesDataFromJson(response.body);
return pagesData;
} catch (e) {
if (e is IOException) {
/*return CountriesData.withError(
'Please check your internet connection.');*/
} else {
print(e.toString());
/*return CountriesData.withError('Something went wrong.');*/
}
}
}
Edit
full code with new sendPagesDataRequest
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_paginator/flutter_paginator.dart';
import 'package:flutter_paginator/enums.dart';
import 'package:cached_network_image/cached_network_image.dart';
// To parse this JSON data, do
//
// final pagesData = pagesDataFromJson(jsonString);
import 'dart:convert';
PagesData pagesDataFromJson(String str) => PagesData.fromJson(json.decode(str));
String pagesDataToJson(PagesData data) => json.encode(data.toJson());
class PagesData {
int currentPage;
List<Datum> data;
String firstPageUrl;
int from;
int lastPage;
String lastPageUrl;
String nextPageUrl;
PagesData({
this.currentPage,
this.data,
this.firstPageUrl,
this.from,
this.lastPage,
this.lastPageUrl,
this.nextPageUrl,
});
factory PagesData.fromJson(Map<String, dynamic> json) => PagesData(
currentPage: json["current_page"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
firstPageUrl: json["first_page_url"],
from: json["from"],
lastPage: json["last_page"],
lastPageUrl: json["last_page_url"],
nextPageUrl: json["next_page_url"],
);
Map<String, dynamic> toJson() => {
"current_page": currentPage,
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"first_page_url": firstPageUrl,
"from": from,
"last_page": lastPage,
"last_page_url": lastPageUrl,
"next_page_url": nextPageUrl,
};
}
class Datum {
int id;
String title;
int likes;
String image;
Datum({
this.id,
this.title,
this.likes,
this.image,
});
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
id: json["id"],
title: json["title"],
likes: json["likes"],
image: json["image"],
);
Map<String, dynamic> toJson() => {
"id": id,
"title": title,
"likes": likes,
"image": image,
};
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Paginator',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<HomePage> {
GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Paginator'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_list_bulleted),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.LIST_VIEW);
},
),
IconButton(
icon: Icon(Icons.grid_on),
onPressed: () {
paginatorGlobalKey.currentState.changeState(
listType: ListType.GRID_VIEW,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
);
},
),
IconButton(
icon: Icon(Icons.library_books),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.PAGE_VIEW);
},
),
],
),
body: Paginator.listView(
key: paginatorGlobalKey,
pageLoadFuture: sendPagesDataRequest,
pageItemsGetter: listItemsGetterPages,
listItemBuilder: listItemBuilder,
loadingWidgetBuilder: loadingWidgetMaker,
errorWidgetBuilder: errorWidgetMaker,
emptyListWidgetBuilder: emptyListWidgetMaker,
totalItemsGetter: totalPagesGetter,
pageErrorChecker: pageErrorChecker,
scrollPhysics: BouncingScrollPhysics(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
paginatorGlobalKey.currentState.changeState(
pageLoadFuture: sendCountriesDataRequest, resetState: true);
},
child: Icon(Icons.refresh),
),
);
}
Future<CountriesData> sendCountriesDataRequest(int page) async {
print('page ${page}');
try {
String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');
http.Response response = await http.get(url);
print('body ${response.body}');
return CountriesData.fromResponse(response);
} catch (e) {
if (e is IOException) {
return CountriesData.withError(
'Please check your internet connection.');
} else {
print(e.toString());
return CountriesData.withError('Something went wrong.');
}
}
}
Future<PagesData> sendPagesDataRequest(int page) async {
print('page ${page}');
try {
/*String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');*/
String url = Uri.encodeFull("https://MY_API_URL?page=$page");
http.Response response = await http.get(url);
print('body ${response.body}');
/*String responseString = '''
{"current_page": 1,
"data": [
{ "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"},
{ "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"}
],
"first_page_url": "https:/API_URL?page=1",
"from": 1,
"last_page": 30,
"last_page_url": "https:/API_URLpage=30",
"next_page_url": "https:/API_URL?page=2"
}
''';*/
PagesData pagesData = pagesDataFromJson(response.body);
return pagesData;
} catch (e) {
if (e is IOException) {
/*return CountriesData.withError(
'Please check your internet connection.');*/
} else {
print(e.toString());
/*return CountriesData.withError('Something went wrong.');*/
}
}
}
List<dynamic> listItemsGetter(CountriesData countriesData) {
List<String> list = [];
countriesData.countries.forEach((value) {
list.add(value['name']);
});
return list;
}
List<dynamic> listItemsGetterPages(PagesData pagesData) {
List<Datum> list = [];
pagesData.data.forEach((value) {
list.add(value);
});
return list;
}
Widget listItemBuilder(dynamic item, int index) {
return Container(
decoration: BoxDecoration(
color: Colors.blue[50]
),
margin: const EdgeInsets.all(8),
child: Column(
children: <Widget>[
new CachedNetworkImage(
imageUrl: item.image,
placeholder: (context, url) => new CircularProgressIndicator(),
errorWidget: (context, url, error) => new Icon(Icons.error),
),
ListTile(title: Text(item.title), subtitle: Text('Likes: ' + item.likes.toString()),),
],),
);
}
Widget loadingWidgetMaker() {
return Container(
alignment: Alignment.center,
height: 160.0,
child: CircularProgressIndicator(),
);
}
Widget errorWidgetMaker(PagesData countriesData, retryListener) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text("error"),
),
FlatButton(
onPressed: retryListener,
child: Text('Retry'),
)
],
);
}
Widget emptyListWidgetMaker(PagesData countriesData) {
return Center(
child: Text('No countries in the list'),
);
}
int totalPagesGetter(PagesData pagesData) {
return pagesData.lastPage;
}
bool pageErrorChecker(PagesData pagesData) {
//return countriesData.statusCode != 200;
return false;
}
}
class CountriesData {
List<dynamic> countries;
int statusCode;
String errorMessage;
int total;
int nItems;
CountriesData.fromResponse(http.Response response) {
this.statusCode = response.statusCode;
List jsonData = json.decode(response.body);
countries = jsonData[1];
total = jsonData[0]['total'];
nItems = countries.length;
}
CountriesData.withError(String errorMessage) {
this.errorMessage = errorMessage;
}
}
Edit
you need to change sendPagesDataRequest, I use static string
Assume your json string like this
{"current_page": 1,
"data": [
{ "id": 1, "title": "Germa", "likes": 5, "image": "image url"},
{ "id": 2, "title": "Jepun", "likes": 3, "image": "image url"}
],
"first_page_url": "https:/API_URL?page=1",
"from": 1,
"last_page": 30,
"last_page_url": "https:/API_URLpage=30",
"next_page_url": "https:/API_URL?page=2"
}
Edit working demo
Edit full code
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_paginator/flutter_paginator.dart';
import 'package:flutter_paginator/enums.dart';
import 'package:cached_network_image/cached_network_image.dart';
// To parse this JSON data, do
//
// final pagesData = pagesDataFromJson(jsonString);
import 'dart:convert';
PagesData pagesDataFromJson(String str) => PagesData.fromJson(json.decode(str));
String pagesDataToJson(PagesData data) => json.encode(data.toJson());
class PagesData {
int currentPage;
List<Datum> data;
String firstPageUrl;
int from;
int lastPage;
String lastPageUrl;
String nextPageUrl;
PagesData({
this.currentPage,
this.data,
this.firstPageUrl,
this.from,
this.lastPage,
this.lastPageUrl,
this.nextPageUrl,
});
factory PagesData.fromJson(Map<String, dynamic> json) => PagesData(
currentPage: json["current_page"],
data: List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
firstPageUrl: json["first_page_url"],
from: json["from"],
lastPage: json["last_page"],
lastPageUrl: json["last_page_url"],
nextPageUrl: json["next_page_url"],
);
Map<String, dynamic> toJson() => {
"current_page": currentPage,
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"first_page_url": firstPageUrl,
"from": from,
"last_page": lastPage,
"last_page_url": lastPageUrl,
"next_page_url": nextPageUrl,
};
}
class Datum {
int id;
String title;
int likes;
String image;
Datum({
this.id,
this.title,
this.likes,
this.image,
});
factory Datum.fromJson(Map<String, dynamic> json) => Datum(
id: json["id"],
title: json["title"],
likes: json["likes"],
image: json["image"],
);
Map<String, dynamic> toJson() => {
"id": id,
"title": title,
"likes": likes,
"image": image,
};
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Paginator',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<HomePage> {
GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Paginator'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_list_bulleted),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.LIST_VIEW);
},
),
IconButton(
icon: Icon(Icons.grid_on),
onPressed: () {
paginatorGlobalKey.currentState.changeState(
listType: ListType.GRID_VIEW,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
);
},
),
IconButton(
icon: Icon(Icons.library_books),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.PAGE_VIEW);
},
),
],
),
body: Paginator.listView(
key: paginatorGlobalKey,
pageLoadFuture: sendPagesDataRequest,
pageItemsGetter: listItemsGetterPages,
listItemBuilder: listItemBuilder,
loadingWidgetBuilder: loadingWidgetMaker,
errorWidgetBuilder: errorWidgetMaker,
emptyListWidgetBuilder: emptyListWidgetMaker,
totalItemsGetter: totalPagesGetter,
pageErrorChecker: pageErrorChecker,
scrollPhysics: BouncingScrollPhysics(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
paginatorGlobalKey.currentState.changeState(
pageLoadFuture: sendCountriesDataRequest, resetState: true);
},
child: Icon(Icons.refresh),
),
);
}
Future<CountriesData> sendCountriesDataRequest(int page) async {
print('page ${page}');
try {
String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');
http.Response response = await http.get(url);
print('body ${response.body}');
return CountriesData.fromResponse(response);
} catch (e) {
if (e is IOException) {
return CountriesData.withError(
'Please check your internet connection.');
} else {
print(e.toString());
return CountriesData.withError('Something went wrong.');
}
}
}
Future<PagesData> sendPagesDataRequest(int page) async {
print('page ${page}');
try {
String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');
http.Response response = await http.get(url);
print('body ${response.body}');
String responseString = '''
{"current_page": 1,
"data": [
{ "id": 1, "title": "Germa", "likes": 5, "image": "https://picsum.photos/250?image=8"},
{ "id": 2, "title": "Jepun", "likes": 3, "image": "https://picsum.photos/250?image=9"}
],
"first_page_url": "https:/API_URL?page=1",
"from": 1,
"last_page": 30,
"last_page_url": "https:/API_URLpage=30",
"next_page_url": "https:/API_URL?page=2"
}
''';
PagesData pagesData = pagesDataFromJson(responseString);
return pagesData;
} catch (e) {
if (e is IOException) {
/*return CountriesData.withError(
'Please check your internet connection.');*/
} else {
print(e.toString());
/*return CountriesData.withError('Something went wrong.');*/
}
}
}
List<dynamic> listItemsGetter(CountriesData countriesData) {
List<String> list = [];
countriesData.countries.forEach((value) {
list.add(value['name']);
});
return list;
}
List<dynamic> listItemsGetterPages(PagesData pagesData) {
List<Datum> list = [];
pagesData.data.forEach((value) {
list.add(value);
});
return list;
}
Widget listItemBuilder(dynamic item, int index) {
return Container(
decoration: BoxDecoration(
color: Colors.blue[50]
),
margin: const EdgeInsets.all(8),
child: Column(
children: <Widget>[
new CachedNetworkImage(
imageUrl: item.image,
placeholder: (context, url) => new CircularProgressIndicator(),
errorWidget: (context, url, error) => new Icon(Icons.error),
),
ListTile(title: Text(item.title), subtitle: Text('Likes: ' + item.likes.toString()),),
],),
);
}
Widget loadingWidgetMaker() {
return Container(
alignment: Alignment.center,
height: 160.0,
child: CircularProgressIndicator(),
);
}
Widget errorWidgetMaker(PagesData countriesData, retryListener) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text("error"),
),
FlatButton(
onPressed: retryListener,
child: Text('Retry'),
)
],
);
}
Widget emptyListWidgetMaker(PagesData countriesData) {
return Center(
child: Text('No countries in the list'),
);
}
int totalPagesGetter(PagesData pagesData) {
return pagesData.lastPage;
}
bool pageErrorChecker(PagesData pagesData) {
//return countriesData.statusCode != 200;
return false;
}
}
class CountriesData {
List<dynamic> countries;
int statusCode;
String errorMessage;
int total;
int nItems;
CountriesData.fromResponse(http.Response response) {
this.statusCode = response.statusCode;
List jsonData = json.decode(response.body);
countries = jsonData[1];
total = jsonData[0]['total'];
nItems = countries.length;
}
CountriesData.withError(String errorMessage) {
this.errorMessage = errorMessage;
}
}
You can use package https://pub.dev/packages/flutter_paginator
It will auto call your REST with page parameter
In the following demo, I add print message , so you can see it auto call rest with page when scroll down
You can copy paste run full code below
code snippet
Future<CountriesData> sendCountriesDataRequest(int page) async {
print('page ${page}');
try {
String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');
http.Response response = await http.get(url);
print('body ${response.body}');
return CountriesData.fromResponse(response);
} catch (e) {
if (e is IOException) {
return CountriesData.withError(
'Please check your internet connection.');
} else {
print(e.toString());
return CountriesData.withError('Something went wrong.');
}
}
}
working demo
full demo code
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:flutter_paginator/flutter_paginator.dart';
import 'package:flutter_paginator/enums.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Paginator',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomeState();
}
}
class HomeState extends State<HomePage> {
GlobalKey<PaginatorState> paginatorGlobalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Paginator'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_list_bulleted),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.LIST_VIEW);
},
),
IconButton(
icon: Icon(Icons.grid_on),
onPressed: () {
paginatorGlobalKey.currentState.changeState(
listType: ListType.GRID_VIEW,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2),
);
},
),
IconButton(
icon: Icon(Icons.library_books),
onPressed: () {
paginatorGlobalKey.currentState
.changeState(listType: ListType.PAGE_VIEW);
},
),
],
),
body: Paginator.listView(
key: paginatorGlobalKey,
pageLoadFuture: sendCountriesDataRequest,
pageItemsGetter: listItemsGetter,
listItemBuilder: listItemBuilder,
loadingWidgetBuilder: loadingWidgetMaker,
errorWidgetBuilder: errorWidgetMaker,
emptyListWidgetBuilder: emptyListWidgetMaker,
totalItemsGetter: totalPagesGetter,
pageErrorChecker: pageErrorChecker,
scrollPhysics: BouncingScrollPhysics(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
paginatorGlobalKey.currentState.changeState(
pageLoadFuture: sendCountriesDataRequest, resetState: true);
},
child: Icon(Icons.refresh),
),
);
}
Future<CountriesData> sendCountriesDataRequest(int page) async {
print('page ${page}');
try {
String url = Uri.encodeFull(
'http://api.worldbank.org/v2/country?page=$page&format=json');
http.Response response = await http.get(url);
print('body ${response.body}');
return CountriesData.fromResponse(response);
} catch (e) {
if (e is IOException) {
return CountriesData.withError(
'Please check your internet connection.');
} else {
print(e.toString());
return CountriesData.withError('Something went wrong.');
}
}
}
List<dynamic> listItemsGetter(CountriesData countriesData) {
List<String> list = [];
countriesData.countries.forEach((value) {
list.add(value['name']);
});
return list;
}
Widget listItemBuilder(value, int index) {
return ListTile(
leading: Text(index.toString()),
title: Text(value),
);
}
Widget loadingWidgetMaker() {
return Container(
alignment: Alignment.center,
height: 160.0,
child: CircularProgressIndicator(),
);
}
Widget errorWidgetMaker(CountriesData countriesData, retryListener) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(countriesData.errorMessage),
),
FlatButton(
onPressed: retryListener,
child: Text('Retry'),
)
],
);
}
Widget emptyListWidgetMaker(CountriesData countriesData) {
return Center(
child: Text('No countries in the list'),
);
}
int totalPagesGetter(CountriesData countriesData) {
return countriesData.total;
}
bool pageErrorChecker(CountriesData countriesData) {
return countriesData.statusCode != 200;
}
}
class CountriesData {
List<dynamic> countries;
int statusCode;
String errorMessage;
int total;
int nItems;
CountriesData.fromResponse(http.Response response) {
this.statusCode = response.statusCode;
List jsonData = json.decode(response.body);
countries = jsonData[1];
total = jsonData[0]['total'];
nItems = countries.length;
}
CountriesData.withError(String errorMessage) {
this.errorMessage = errorMessage;
}
}
Output
I/flutter (20369): page 1
I/flutter (20369): body [{"page":1,"pages":7,"per_page":"50","total":304},[{"id":"ABW","iso2Code":"AW","name":"Aruba","region":{"id":"LCN","iso2code":"ZJ","value":"Latin America & Caribbean "},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"HIC","iso2code":"XD","value":"High income"},"lendingType":{"id":"LNX","iso2code":"XX","value":"Not classified"},"capitalCity":"Oranjestad","longitude":"-70.0167","latitude":"12.5167"},{"id":"AFG","iso2Code":"AF","name":"Afghanistan","region":{"id":"SAS","iso2code":"8S","value":"South Asia"},"adminregion":{"id":"SAS","iso2code":"8S","value":"South Asia"},"incomeLevel":{"id":"LIC","iso2code":"XM","value":"Low income"},"lendingType":{"id":"IDX","iso2code":"XI","value":"IDA"},"capitalCity":"Kabul","longitude":"69.1761","latitude":"34.5228"},{"id":"AFR","iso2Code":"A9","name":"Africa","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"NA","iso2code":"NA","value":"Aggregates"},"lendingType":{"id":""
I/flutter (20369): page 2
I/flutter (20369): body [{"page":2,"pages":7,"per_page":"50","total":304},[{"id":"CIV","iso2Code":"CI","name":"Cote d'Ivoire","region":{"id":"SSF","iso2code":"ZG","value":"Sub-Saharan Africa "},"adminregion":{"id":"SSA","iso2code":"ZF","value":"Sub-Saharan Africa (excluding high income)"},"incomeLevel":{"id":"LMC","iso2code":"XN","value":"Lower middle income"},"lendingType":{"id":"IDX","iso2code":"XI","value":"IDA"},"capitalCity":"Yamoussoukro","longitude":"-4.0305","latitude":"5.332"},{"id":"CLA","iso2Code":"C6","name":"Latin America and the Caribbean (IFC classification)","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","value":""},"incomeLevel":{"id":"NA","iso2code":"NA","value":"Aggregates"},"lendingType":{"id":"","iso2code":"","value":"Aggregates"},"capitalCity":"","longitude":"","latitude":""},{"id":"CME","iso2Code":"C7","name":"Middle East and North Africa (IFC classification)","region":{"id":"NA","iso2code":"NA","value":"Aggregates"},"adminregion":{"id":"","iso2code":"","va
25 Comments
Infinite Scrolling Pagination is a tough task.
Besides just fetching new items lazily, you want to keep the user posted on your current state. For example, if you're loading the first page, you might want to show a progress indicator in the middle of the screen. But, if you're loading a subsequent page, you probably want to show a progress indicator at the bottom. The same is true for error indicators.
You also need to stop requesting new pages if the list from the server is either empty or completed. Not even to mention that you probably want to add "retry" buttons for failed requests.
There's now a package called Infinite Scroll Pagination that can handle everything for you, and the usage is pretty simple. To showcase that, I'll use the same country list example from @chunhunghan answer:
class CountryListView extends StatefulWidget {
@override
_CountryListViewState createState() => _CountryListViewState();
}
class _CountryListViewState extends State<CountryListView> {
static const _pageSize = 20;
final PagingController<int, Country> _pagingController =
PagingController(firstPageKey: 0);
@override
void initState() {
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
super.initState();
}
void _fetchPage(int pageKey) {
RemoteApi.getCountryList(pageKey, _pageSize).then((newItems) {
final isLastPage = newItems.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + newItems.length;
_pagingController.appendPage(newItems, nextPageKey);
}
}).catchError((error) {
_pagingController.error = error;
});
}
@override
Widget build(BuildContext context) => PagedListView<int, Country>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Country>(
itemBuilder: (context, item, index) => CountryListItem(
country: item,
),
),
);
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}
In the code above, all of the issues I listed in the beginning (and others) are addressed, and you can customize everything if you need.
Disclosure: I'm the package author, so feel free to message me with any doubts you may have.
I have created a lightweight example of an infinite loading list with pagination. New items are requested as you reach the bottom of the list. Usage looks like this:
import 'package:flutter/material.dart';
class Example extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InfiniteList(
widgetBuilder: (item) {
return Text(item);
},
loadMore: (lastLoaded) {
if (lastLoaded == null) {
//first load request
return ["hello", "world"];
} else {
//subsequent load request(s)
return [];
}
},
onItemSelected: (item) {
print(item);
},
);
}
}
The idea is to paginate based on the last loaded item, lastLoaded rather than a page number. Doing this helps to ensure you don't miss or duplicate anything if the contents of page X+1 changes after you already loaded page X (i.e. when something is added or removed from the database).
If your API doesn't support that, or you don't want it, you could add a page number attribute to each of your items and then do:
something.load(page: lastLoaded.pageNumber + 1);
The implementation for InfiniteList looks like this:
import 'package:flutter/material.dart';
extension on List {
Object lastOrNull() {
return this.isNotEmpty ? this.last : null;
}
}
typedef ItemWidgetBuilder = Widget Function(Object item);
typedef FutureItemsCallback = Future<List<Object>> Function(Object lastLoadedItem);
typedef ItemCallback = void Function(Object item);
class InfiniteList extends StatefulWidget {
final ItemWidgetBuilder widgetBuilder;
final FutureItemsCallback loadMore;
final ItemCallback onItemSelected;
InfiniteList({Key key, @required this.widgetBuilder, @required this.loadMore, this.onItemSelected}) : super(key: key);
@override
State<StatefulWidget> createState() {
return InfiniteListState();
}
}
class InfiniteListState extends State<InfiniteList> {
List<Object> items = [];
bool shouldTryToLoadMore = true;
@override
void initState() {
super.initState();
waitOnItems();
}
void waitOnItems() async {
try {
final items = await widget.loadMore(this.items.lastOrNull());
this.shouldTryToLoadMore = items.isNotEmpty;
setState(() {
this.items.addAll(items);
});
} catch(error) {
print(error);
}
}
@override
Widget build(BuildContext context) {
if (items.isEmpty) {
return initiallyLoading();
} else {
//TODO: show progress bar at the bottom if loading more
return list();
}
}
Widget list() {
return ListView.builder(
itemCount: shouldTryToLoadMore ? null : items.length,
itemBuilder: (context, index) {
if (shouldTryToLoadMore && index == items.length - 1) {
waitOnItems();
return null;
} else if (index >= items.length) {
return null;
} else if (widget.onItemSelected != null) {
return InkWell(
onTap: () => {
widget.onItemSelected(items[index])
},
child: widget.widgetBuilder(items[index]),
);
} else {
return widget.widgetBuilder(items[index]);
}
}
);
}
Widget initiallyLoading() {
return Center(
child: CircularProgressIndicator(),
);
}
}
A full gist is here: https://gist.github.com/tombailey/988f788493cec9b95e7e9e007b8a7a0d
1 Comment
screen_movie_listing.dart
import 'package:flutter/material.dart';
class ScreenMovieListing extends StatefulWidget {
const ScreenMovieListing({Key? key}) : super(key: key);
@override
State<ScreenMovieListing> createState() => _ScreenMovieListingState();
}
class _ScreenMovieListingState extends State<ScreenMovieListing> {
final _scrollController = ScrollController();
bool isLoading = false;
bool isLastPage = false;
int currentPage = 0;
int totalPage = 0;
int totalItems = 0;
var moviesList = [];
@override
void initState() {
_restAPICall(true);
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
if (!isLastPage) {
_restAPICall(false);
}
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _restAPICall(bool clearList) async {
if (clearList) {
currentPage = 1;
moviesList.clear();
}
setState(() {
isLoading = true;
});
await RestAPIHelper.movieList(
context: context,
currentPage: currentPage.toString(),
).then((response) {
setState(() {
isLoading = false;
});
if (response.status!) {
setState(() {
currentPage = response.pagignation!.currentPage!;
totalPage = response.pagignation!.totalPages!;
totalItems = response.pagignation!.totalItems!;
if (currentPage == totalPage) {
isLastPage = true;
} else {
currentPage = currentPage + 1;
}
moviesList.addAll(response.moviesListFromAPIResponse!);
if (_scrollController.position.extentAfter <= 0 && isLoading == false) {
// This code will call the pagination API if more data is available and screen had covered content
if (!isLastPage) {
_restAPICall(false);
}
}
});
}
}).onError((error, stackTrace) {
setState(() {
isLoading = false;
});
});
}
Widget _loadMoreIndicator() {
return !isLastPage
? const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Center(
child: CircularProgressIndicator(),
),
)
: const SizedBox(height: 0);
}
@override
Widget build(BuildContext context) {
return ListView.separated(
controller: _scrollController,
shrinkWrap: true,
itemCount: moviesList.length + 1,
separatorBuilder: (_ctx, _index) {
return SizedBox(height: 20);
},
itemBuilder: (_ctx, _index) {
if (_index == moviesList.length) {
return _loadMoreIndicator();
} else {
return ListItemMovie(data: moviesList[_index]);
}
},
);
}
}
rest_api_helper.dart
class RestAPIHelper {
static Future<MoviesListModel> movieList(
{BuildContext? context, String? currentPage = ''}) async {
MoviesListModel? data;
var bodyData = {
'user_id': '1',
'current_page': currentPage,
'item_per_page': '1',
};
Map<String, String>? headerData = {
'token': '1234567890',
};
try {
final response = await http.post('api_url', headers: headerData, body: bodyData);
var decodedResult = jsonDecode(response.body);
data = MoviesListModel.fromJson(decodedResult);
return data;
} on SocketException {
return null;
} catch (error) {
return null;
}
return data!;
}
}
API Response
{
"status": true,
"message": "Movies Listing",
"pagignation": {
"current_page": 1,
"total_pages": 7,
"total_items": 7
},
"data": [
{
"id": "1",
"name": "Movie Name",
"image": "",
"genre": "",
"language": "Hindi",
"release_year": "2022"
}
]
}
This example is based on API which have request parameter such as current_page, item_per_page & have response parameter such as current_page, total_pages, total_items
Comments
I use ScrollController in Pagination ::
ScrollController? scrollController;
inside initState initialize it and pass the scrollListener function::
scrollController = ScrollController()..addListener(_scrollListener);
here is _scrollListener ::
- when extentAfter becomes 0 which means reached the bottom of last item you
call API again but you should also check if last page reached so that you
do not call API again when you reach last page , it is hashed inside the
condition
void _scrollListener() {
debugPrint("extentAfter ::::: " +
scrollController!.position.extentAfter.toString());
if (scrollController!.position.extentAfter <= 0 /*&&
viewModel.pageNumber < viewModel.totalCount + 1*/) {
if (!viewModel.lazyLoading) {
viewModel.lazyLoading = true; //show a loading indicator
// call APi again and inside getNewProducts "${pageNumber++}"
viewModel.getNewProducts(viewModel.catId ?? "");
}
}
}
then you pass scrollController to the listView/GridView controller ::
controller: scrollController,
Comments
If I had to implement the pagination myself, I would either use infinite_scroll_pagination library or, if the project is small and I just want it in one place and don't want to add yet another dependency, I would just wrap my ListView with NotificationListener<ScrollNotification> like below:
NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) {
onScrollListener();
}
return true;
}
void onScrollListener() {
if (reachedEnd) {
return;
}
if (!isPageLoading) {
isPageLoading = true;
Future.microtask(() async {
final newItems = await getItemsUseCase.get(page: nextPage);
if (newItems.length < pageSize) {
reachedEnd= true;
} else {
nextPage++;
}
allItems.addAll(newItems);
isPageLoading = false;
});
}
}
I've added a complete code example here.
getJSONData(int page)as a result of callingitemBuilderand cache it somewhere (for example usingMapCache)