I am new in flutter. I want to do the pagination with REST API. My question is how to add 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? Anyone can help me? Thank you very much
问题:
回答1:
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
回答2:
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