Reputation: 6082
I'm new at flutter and I have been searching for proper result of pagination for 2 days.
I followed this code Flutter ListView lazy loading But didn't achieve what I want. Look at below code.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
main() => runApp(InitialSetupPage());
class InitialSetupPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "API Pagination",
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.green),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int pageNum = 1;
bool isPageLoading = false;
List<Map<String, dynamic>> arrayOfProducts;
Future<List<Map<String, dynamic>>> _callAPIToGetListOfData() async {
isPageLoading = true;
final responseDic =
await http.post(urlStr, headers: accessToken, body: paramDic);
Map<String, dynamic> dicOfRes = json.decode(responseDic.body);
List<Map<String, dynamic>> temArr = List<Map<String, dynamic>>.from(dicOfRes["data"]["products"]);
if (pageNum == 1) {
arrayOfProducts = temArr;
} else {
arrayOfProducts.addAll(temArr);
}
return arrayOfProducts;
}
ScrollController controller;
@override
void initState() {
controller = new ScrollController()..addListener(_scrollListener);
super.initState();
}
_scrollListener() {
print(controller.position.extentAfter);
if (controller.position.extentAfter <= 0 && isPageLoading == false) {
_callAPIToGetListOfData().then((data){
setState(() {
});
});
}
}
@override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Online Products"),
centerTitle: true,
),
body: Container(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: _callAPIToGetListOfData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
if (snapshot.hasError) {
Text(
'YOu have some error : ${snapshot.hasError.toString()}');
} else if (snapshot.data != null) {
isPageLoading = false;
pageNum++;
print(arrayOfProducts);
return Scrollbar(
child: ListView.builder(
itemCount: arrayOfProducts.length,
controller: controller,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(
title: Text(
'$index ${arrayOfProducts[index]['title']}'),
);
}),
);
}
}
}),
));
}
}
So, When I reach at bottom of the page my _scrollListener
method get call and there I have set setState(()....
method to reload widget. Issue is I load my actual position and It starts with top of the list. So where I'm going wrong? Actually I want like. https://github.com/istyle-inc/LoadMoreTableViewController/blob/master/screen.gif
Final code: (Guide by @Rémi Rousselet)
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
main() => runApp(InitialSetupPage());
class InitialSetupPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "API Pagination",
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.green),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int pageNum = 1;
bool isPageLoading = false;
List<Map<String, dynamic>> arrayOfProducts;
ScrollController controller;
Future<List<Map<String, dynamic>>> future;
int totalRecord = 0;
@override
void initState() {
controller = new ScrollController()..addListener(_scrollListener);
future = _callAPIToGetListOfData();
super.initState();
}
@override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final constListView = ListView.builder(
itemCount: arrayOfProducts == null ? 0 : arrayOfProducts.length,
controller: controller,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Column(
children: <Widget>[
ListTile(
title: Text('$index ${arrayOfProducts[index]['title']}'),
leading: CircleAvatar(backgroundImage: NetworkImage(arrayOfProducts[index]['thumbnail'] ?? "")),
),
Container(
color: Colors.black12,
height: (index == arrayOfProducts.length-1 && totalRecord > arrayOfProducts.length) ? 50 : 0,
width: MediaQuery.of(context).size.width,
child:Center(
child: CircularProgressIndicator()
),
)
],
);
});
return Scaffold(
appBar: AppBar(
title: Text("Online Products"),
centerTitle: true,
),
body: Container(
child: FutureBuilder<List<Map<String, dynamic>>>(
future: future,
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
if (snapshot.hasError) {
Text(
'YOu have some error : ${snapshot.hasError.toString()}');
} else if (snapshot.data != null) {
isPageLoading = false;
print(arrayOfProducts);
return constListView;
}
}
}),
));
}
Future<List<Map<String, dynamic>>> _callAPIToGetListOfData() async {
isPageLoading = true;
final responseDic =
await http.post(urlStr, headers: accessToken, body: paramDic);
Map<String, dynamic> dicOfRes = json.decode(responseDic.body);
List<Map<String, dynamic>> temArr =
List<Map<String, dynamic>>.from(dicOfRes["data"]["products"]);
setState(() {
if (pageNum == 1) {
totalRecord = dicOfRes["total_record"];
print('============>>>>>>> $totalRecord');
arrayOfProducts = temArr;
} else {
arrayOfProducts.addAll(temArr);
}
pageNum++;
});
return arrayOfProducts;
}
_scrollListener() {
if (totalRecord == arrayOfProducts.length) {
return;
}
print(controller.position.extentAfter);
if (controller.position.extentAfter <= 0 && isPageLoading == false) {
_callAPIToGetListOfData();
}
}
}
Thats working but Is it right/good way? little bit confused because if I reach at end of the page and scroll up/down it seems little bit sticky while scrolling..
Upvotes: 28
Views: 69697
Reputation: 1176
You can check by either using ScrollController
with maxScrollExtent
and offset
or by calculating with the help of index
...
let's see the first approach...
set ScrollController()
inside the class
final _scrollController = ScrollController();
Set this ListView
controller inside the ListView.builder
method...
controller: _scrollController
add listener inside the init method...
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
print("list view scrolling");
}
now u can listen to listview scrolling inside _onscroll
method ...
with the following code you can check how much you can scroll on this page with the help of _scrollController.position.maxScrollExtent
and your current scrolling position with the help of offset
... you can also play with the last return currentScroll >= (maxScroll * 0.9)
by changing the value of 0.9 to 0.8 or less if you want to fetch a little bit early before user hit the listview
bottom ...
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
now you can use _isButtom
inside your _onScroll()
method...
void _onScroll() {
if (_isBottom) FetchedMore());
//set currentPage+1 before making API call
//or set startIndex to listViewData.length
}
use context.read
for fetching more content if you are using some state manager i.e bloc / riverpod ... otherwise you have to put some extra logic to prevent sending multiple API calls for same page...
You may be wondering which page to fetch.
just add currentPage+1
every time inside the FetchMore
method if you are using page base pagination or set this ListView
index if you are using startIndex
approach according to your API needs...
now let's see the second approach calculating with help in current Index
with index
calculate if you need to fetch next page like follow ...
void getMoreData(int index) {
const pageSize = 10;
final currentPosition = index + 1;
//check if you need to loadMore
final loadMore = currentPosition % pageSize == 0 && index != 0;
//check which page needs to catch data
final int currentPageToRequest = currentPosition ~/ 10;
if (loadMore && currentPageToRequest > state.pageNo) {
state.copyWith(pageNo: state.pageNo + 1); //update the current page no
getDataWithPage();
}
}
call this method inside ListView
inside itemBuilder
and pass the index...
Upvotes: 0
Reputation: 349
Easiest way without need of any scrollcontroller:
int offset = 0;
int limit = 10;
int posts = [];
getPosts(){
//get more posts and add them to posts array then increment skip = skip + limit
}
ListView.builder(
itemCount: posts.length,
itemBuilder: (BuildContext context, int index) {
if (index == posts.length - 1) {
getPosts();
}
return Column(
children: [
yourItemWidget(posts[index]),
if (index == posts.length - 1)
CupertinoActivityIndicator(),
if (index == posts.length - 1)
SizedBox(
height: 100,
)
],);
},
),
Upvotes: 0
Reputation: 332
https://pub.dev/packages/pagination_view is the package I found the more intuitive to work with that needed the least changes from a page view and felt the more intuitive:
It's easy to move from a listview to a PaginationView, the class that holds the widget needs to be stateful or it won't work:
PaginationView<User>(
preloadedItems: <User>[
User(faker.person.name(), faker.internet.email()),
User(faker.person.name(), faker.internet.email()),
],
itemBuilder: (BuildContext context, User user, int index) => ListTile(
title: Text(user.name),
subtitle: Text(user.email),
leading: IconButton(
icon: Icon(Icons.person),
onPressed: () => null,
),
),
paginationViewType: PaginationViewType.listView // optional
pageFetch: pageFetch,
onError: (dynamic error) => Center(
child: Text('Some error occured'),
),
onEmpty: Center(
child: Text('Sorry! This is empty'),
),
bottomLoader: Center( // optional
child: CircularProgressIndicator(),
),
initialLoader: Center( // optional
child: CircularProgressIndicator(),
),
),
Upvotes: 3
Reputation: 1
Try using this package, https://pub.dev/packages/paginate_firestore this made my day. You just need to add 2 attributes and its done!
It provides output in documentSnapshot and I tried it on ListView(works good) GridView has not been released till now as per contributor.
Upvotes: 0
Reputation: 7209
Add this to your listener of controller
if (scrollController.position.maxScrollExtent == scrollController.offset) {
/***
* we need to get new data on page scroll end but if the last
* time when data is returned, its count should be Constants.itemsCount' (10)
*
* So we calculate every time
*
* productList.length >= (Constants.itemsCount*pageNumber)
*
* list must contain the products == Constants.itemsCount if the page number is 1
* but if page number is increased then we need to calculate the total
* number of products we have received till now example:
* first time on page scroll if last count of productList is Constants.itemsCount
* then increase the page number and get new data.
* Now page number is 2, now we have to check the count of the productList
* if it is==Constants.itemsCount*pageNumber (20 in current case) then we have
* to get data again, if not then we assume, server has not more data then
* we currently have.
*
*/
if (productList.length >= (Constants.itemsCount * pageNumber) &&
!isLoading) {
pageNumber++;
print("PAGE NUMBER $pageNumber");
print("getting data");
getProducts(false); // Hit API to get new data
}
}
Upvotes: 14
Reputation: 1550
https://pub.dartlang.org/packages/loadmore
you can use this package, or you can look at the code and redo it to suit your needs
Upvotes: 0