Closed AndersonArvando closed 8 months ago
@EdsonBueno @clragon can help to solve my issue please
Yeah I'm also looking for solution to this issue. Please kindly share if you see any.
Hi @AndersonArvando. I tried to replicate your issue using this piece of code:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: CustomScrollView(
slivers: [
SliverList(
delegate: SliverChildListDelegate([
const Center(
child: Column(
children: [
MyPagedListView(),
],
),
),
]),
)
],
),
),
),
);
}
}
class MyPagedListView extends StatefulWidget {
const MyPagedListView({
super.key,
});
@override
State<MyPagedListView> createState() => _MyPagedListViewState();
}
class _MyPagedListViewState extends State<MyPagedListView> {
late final PagingController<int, int> _pagingController;
@override
void initState() {
_pagingController = PagingController(firstPageKey: 0);
_pagingController.addPageRequestListener((pageKey) {
print('fetch');
_fetchPage(pageKey);
});
super.initState();
}
Future<void> _fetchPage(int pageKey) async {
const pageSize = 20;
List<int> newItems = [];
for (int i = pageSize * pageKey; i < pageSize; i++) {
newItems.add(i);
}
_pagingController.appendPage(newItems, pageKey++);
}
@override
Widget build(BuildContext context) {
return PagedListView(
shrinkWrap: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (context, data, index) => Container(
height: 100.0,
color: Colors.red,
child: Text('Child $index'),
),
),
);
}
}
The widget tree is currently represented as: CustomScrollView -> SliverList -> Column -> PagedListView
However, when using CustomScrollViews, it's recommended to use PagedSliverList instead.
So, it would be like:
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: CustomScrollView(
slivers: [
MyPagedSliverList(),
],
),
),
),
);
}
}
class MyPagedSliverList extends StatefulWidget {
const MyPagedSliverList({
super.key,
});
@override
State<MyPagedSliverList> createState() => _MyPagedSliverListState();
}
class _MyPagedSliverListState extends State<MyPagedSliverList> {
late final PagingController<int, int> _pagingController;
@override
void initState() {
_pagingController = PagingController(firstPageKey: 0);
_pagingController.addPageRequestListener((pageKey) {
print('fetch');
_fetchPage(pageKey);
});
super.initState();
}
Future<void> _fetchPage(int pageKey) async {
const pageSize = 20;
List<int> newItems = [];
for (int i = pageSize * pageKey; i < pageSize; i++) {
newItems.add(i);
}
_pagingController.appendPage(newItems, pageKey++);
}
@override
Widget build(BuildContext context) {
return PagedSliverList(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (context, data, index) => Container(
height: 100.0,
color: Colors.red,
child: Text('Child $index'),
),
),
);
}
}
Now, the widget tree is something similar to this: CustomScrollView -> PagedSliverList
I hope this helps to solve your problem.
@caiombueno
Thanks for your suggestion, but your solution doesn't work for me.
I've tried it. I am not using a CustomScrollView
though so I don't know if that has something to do with it or not.
Here is how my code currently looks like:
class ViewAllSubscriptions extends StatefulWidget {
final int streetId;
final String status;
final String month;
final String streetName;
final String area;
const ViewAllSubscriptions({
Key? key,
required this.streetId,
required this.month,
required this.status,
required this.streetName,
required this.area,
}) : super(key: key);
@override
ViewAllSubscriptionsState createState() => ViewAllSubscriptionsState();
}
class ViewAllSubscriptionsState extends State<ViewAllSubscriptions> {
static const _pageSize = 3;
String _selectedItem = "ASC";
String searchText = "";
String previousSearchText = "";
bool searchMode = false;
TextEditingController searchController = TextEditingController();
final PagingController<int, dynamic> _pagingController = PagingController(
firstPageKey: 1,
);
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
initPSPSubscriptionDetails(ProfileViewModel model, int pageKey) async {
bool isLastPage = await model.fetchPSPSubscriptionDetails(
widget.streetId,
widget.month,
widget.status,
searchText,
Pagination(
page: pageKey,
limit: _pageSize,
orderBy: widget.status == "paid" ? "createdAt" : "houseNumber",
orderDir: _selectedItem,
),
);
if (isLastPage) {
_pagingController.appendLastPage(model.subDetails);
} else {
_pagingController.appendPage(
model.subDetails,
pageKey + 1,
);
}
}
@override
Widget build(BuildContext context) {
return BaseWidget<ProfileViewModel>(
viewModelBuilder: () => ProfileViewModel(context),
onStart: (model) {
initPSPSubscriptionDetails(model, 1);
_pagingController.addPageRequestListener((pageKey) async {
initPSPSubscriptionDetails(model, pageKey);
});
},
builder: (context, model, wgt) {
if (!model.canDisplayPage) {
return const CustomLoader();
}
return RefreshIndicator(
child: Scaffold(
appBar: AppBar(
scrolledUnderElevation: 0,
toolbarHeight: 10.h,
centerTitle: true,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
model.canDisplayPage = true;
Navigator.pop(context);
},
),
title: Column(
children: [
Container(
alignment: Alignment.bottomCenter,
child: Text(
widget.streetName,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Center(
child: Text(
widget.area,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
).opacity(opacity: 0),
],
),
body: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: model.subDetails.isEmpty && !searchMode
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [
model.subDetails.isEmpty && !searchMode
? const SizedBox(height: 0)
: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: 40.w,
color: AppColors.kColorGrey,
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
searchText.isNotEmpty
? IconButton(
icon: const Icon(Icons.close_rounded),
onPressed: () {
searchController.text = "";
bool previousSearchMode = searchMode;
setState(() {
searchMode = false;
searchText = "";
previousSearchText = "";
});
if (previousSearchMode) {
if (_pagingController.nextPageKey !=
_pagingController
.firstPageKey) {
_pagingController.refresh();
if (model.subDetails.isEmpty) {
initPSPSubscriptionDetails(
model,
1,
);
}
} else {
initPSPSubscriptionDetails(
model,
1,
);
}
}
},
).withWidth((2.5).h)
: const SizedBox(),
FormItem(
backgroundColor: Colors.transparent,
isDesignTwo: true,
maxLines: 1,
placeholder: 'Enter House No',
label: 'Enter House No',
controller: searchController,
onChange: (val) {
setState(() => searchText = val);
if (val.isEmpty) {
bool previousSearchMode = searchMode;
setState(() {
searchMode = false;
searchText = "";
previousSearchText = "";
});
if (previousSearchMode) {
if (_pagingController.nextPageKey !=
_pagingController.firstPageKey) {
_pagingController.refresh();
if (model.subDetails.isEmpty) {
initPSPSubscriptionDetails(
model,
1,
);
}
} else {
initPSPSubscriptionDetails(
model,
1,
);
}
}
}
},
keyboardType: EnumKeyboardType.number,
).expand(),
Container(
color: Colors.black,
child: IconButton(
onPressed: () {
if (searchText.isNotEmpty &&
(previousSearchText.isEmpty ||
previousSearchText !=
searchText)) {
setState(() {
searchMode = true;
previousSearchText = searchText;
});
if (_pagingController.nextPageKey !=
_pagingController.firstPageKey) {
_pagingController.refresh();
if (model.subDetails.isEmpty) {
initPSPSubscriptionDetails(
model,
1,
);
}
} else {
initPSPSubscriptionDetails(model, 1);
}
}
},
icon: Icon(
Icons.search,
color: AppColors.kColorWhite,
),
),
)
],
),
).cornerRadiusWithClipRRect(5.sp).withWidth(210),
Row(
children: [
Text(
"Total: ${model.subDetailsTotal}",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
).paddingRight(2.h),
DropdownButtonHideUnderline(
child: DropdownButton2(
customButton:
const Icon(Icons.more_vert_outlined),
items: [
DropdownMenuItem<String>(
enabled: _selectedItem == "DESC",
value: "ASC",
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
"Sort by ASC",
style: TextStyle(
fontSize: 8.sp,
),
).expand(),
if (_selectedItem == "ASC")
const Icon(Icons.check),
],
),
),
DropdownMenuItem<String>(
enabled: _selectedItem == "ASC",
value: "DESC",
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
"Sort by DESC",
style: TextStyle(fontSize: 8.sp),
).expand(),
if (_selectedItem == "DESC")
const Icon(Icons.check),
],
),
),
],
onChanged: (value) {
_selectedItem = value!;
_pagingController.refresh();
if (model.subDetails.isEmpty) {
initPSPSubscriptionDetails(model, 1);
}
},
dropdownStyleData: DropdownStyleData(
width: 160,
padding: const EdgeInsets.symmetric(
vertical: 0,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.white,
),
offset: const Offset(0, 8),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.only(
left: 16,
right: 16,
),
),
),
),
],
)
],
)
.withHeight(5.h)
.withWidth(100.w)
.paddingSymmetric(vertical: 1.h, horizontal: 5.w),
model.subDetails.isEmpty
? const SizedBox(height: 0)
: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
RichText(
text: TextSpan(
children: [
TextSpan(
text: "₦ ",
style: TextStyle(
color: Colors.black,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
TextSpan(
text: model.subDetails.isEmpty ||
widget.status != 'paid'
? '0.00'
: MoneyFormatter(
amount: model.subDetails
.fold(
0,
(t, e) =>
t +
(e['payment']
['serviceCharge']
as int),
)
.toDouble(),
).output.nonSymbol,
style: Theme.of(context)
.textTheme
.headlineMedium,
)
],
),
),
WasteNgButton(
text: widget.status.capitalizeFirstLetter(),
onPressed: () {},
backgroundColor: widget.status == "paid"
? AppColors.kColorPrimary
: Colors.red,
),
],
)
.withHeight(5.h)
.withWidth(100.w)
.paddingSymmetric(vertical: 1.h, horizontal: 5.w),
model.subDetails.isNotEmpty
? const SizedBox(height: 0)
: Center(
child: Text(
"No Record",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.sp,
),
),
).paddingBottom(30.h).paddingTop(
model.subDetails.isEmpty && !searchMode ? 0.h : 10.h,
),
model.subDetails.isEmpty
? const SizedBox(height: 0)
: PagedSliverList<int, dynamic>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<dynamic>(
itemBuilder: (context, item, index) {
return _WidgetSubscription(
item: item,
status: widget.status,
);
},
),
).expand()
],
),
),
onRefresh: () async {
searchController.text = "";
setState(() => searchText = "");
await Future.delayed(const Duration(seconds: 2));
_pagingController.refresh();
if (model.subDetails.isEmpty) {
initPSPSubscriptionDetails(model, 1);
}
},
);
},
);
}
}
I'm sorry if it's kind of long, just want to show the whole file so nothing is out of context. As shown in the above code, i am using PagedSliverList
like you suggested and yet the listener keeps querying the next page even before the user scrolls.
Hi @Israel001. From what I can see, you are using a sliver PagedSliverList
within a Column
.
Flutter follows two protocols for drawing widgets: Sliver and Box protocols.
A Column
expects its children to adhere to the Box protocol. However, in the case of a PagedSliverList
, it uses the Sliver protocol.
If you want to use a paged list inside a Column
, I would recommend using a PagedListView wrapped in an Expanded widget (to avoid an unbounded height), and make sure to turn off shrinkWrap.
Let me know if it doesn't work.
@caiombueno God bless you bro ❤️
It works!!!
Turning off the shrinkWrap is what did it for me 'cause I was already using Column and Expanded before but shrinkWrap was on.
Thank you very much bro.
I hope the original issue poster will also get his issue resolved.
@caiombueno Hello,
Sorry to disturb you again (:
After replicating the pagination logic as you explained for another scenario, it didn't work. I am getting the same issue as before, where the new pages are fetched before i even scroll.
I am using column inside a IndexedStack Widget. Here is an overview of my widget flow from scaffold to the pagination widget:
Scaffold -> IndexedStack -> Column -> Expanded -> Padding -> PagedListView -> Container -> Column
The Scaffold and IndexedStack is in a different file which imports the 1st Column Widget from another file.
Here is the code for the file that contains the PagedListView widget:
class CollectionHistoryViewState extends State<CollectionHistoryView> {
final PagingController<int, UpcomingCollectionRequest>
_upcomingCollectionsController = PagingController(firstPageKey: 1);
final PagingController<int, CollectionRequest> _collectionsTodayController =
PagingController(firstPageKey: 1);
@override
void dispose() {
_upcomingCollectionsController.dispose();
_collectionsTodayController.dispose();
super.dispose();
}
fetchUpcomingCollections(Pagination pagination, int nextPageKey) async {
try {
String? accessToken = await getIt<StorageService>().getFromDisk(
Constants.accessToken,
);
Response response = await getIt<Dio>().get(
ServerRoutes.getUpcomingCollections(pagination),
options: Options(
headers: {"Authorization": "Bearer $accessToken"},
),
);
bool isLastPage = response.data['pagination']['page'] ==
response.data['pagination']['pages'];
List<UpcomingCollectionRequest> upcomingCollections =
(response.data['data'] as List)
.map((value) => UpcomingCollectionRequest.fromJson(value))
.toList();
if (isLastPage) {
_upcomingCollectionsController.appendLastPage(upcomingCollections);
} else {
_upcomingCollectionsController.appendPage(
upcomingCollections,
nextPageKey,
);
}
} on DioException catch (e) {
if (context.mounted) handleDioException(e, context);
}
}
fetchCollectionsToday(Pagination pagination, int nextPageKey) async {
try {
String? accessToken = await getIt<StorageService>().getFromDisk(
Constants.accessToken,
);
Response response = await getIt<Dio>().get(
ServerRoutes.getCollectionsToday(pagination),
options: Options(
headers: {"Authorization": "Bearer $accessToken"},
),
);
bool isLastPage = response.data['pagination']['page'] ==
response.data['pagination']['pages'];
List<CollectionRequest> collectionsToday = (response.data['data'] as List)
.map((value) => CollectionRequest.fromJson(value))
.toList();
if (isLastPage) {
_collectionsTodayController.appendLastPage(collectionsToday);
} else {
_collectionsTodayController.appendPage(
collectionsToday,
nextPageKey,
);
}
} on DioException catch (e) {
if (context.mounted) handleDioException(e, context);
}
}
@override
void initState() {
widget.totalUpcomingCollections > widget.upcomingCollections.length
? _upcomingCollectionsController.appendPage(
widget.upcomingCollections,
2,
)
: _upcomingCollectionsController
.appendLastPage(widget.upcomingCollections);
widget.totalCollectionsToday > widget.collectionsToday.length
? _collectionsTodayController.appendPage(widget.collectionsToday, 2)
: _collectionsTodayController.appendLastPage(widget.collectionsToday);
_collectionsTodayController.addPageRequestListener((pageKey) {
fetchCollectionsToday(Pagination(limit: 5, page: pageKey), pageKey);
});
_upcomingCollectionsController.addPageRequestListener((pageKey) {
print("here???");
fetchUpcomingCollections(Pagination(limit: 5, page: pageKey), pageKey);
});
super.initState();
}
@override
Widget build(BuildContext context) {
final model = context.watch<ProfileViewModel>();
int listCount = model.collectionState == "Collections"
? model.collectionState2 == 'Collections Today'
? model.collectionsToday.length
: model.upcomingCollectionReqs.length
: 0;
String listInUse = "";
String collectionListInUse =
model.collectionState2 == 'Collections Today' ? "list" : "upcomingList";
Map baseList = {};
if (listCount < 1) {
switch (model.collectionState) {
case "Collections":
listCount = model.collectionState2 == 'Collections Today'
? model.collectionsToday.length
: model.upcomingCollectionReqs.length;
collectionListInUse = model.collectionState2 == 'Collections Today'
? "list"
: "upcomingList";
break;
case "Missed":
listCount = model.collectionState2 == 'This Week'
? model.missedCollectionReqs.length
: model.missedCollectionHistory.entries.length;
listInUse = model.collectionState2 == 'This Week'
? "missedThisWeek"
: "historyList";
if (listInUse == 'historyList') {
baseList = model.missedCollectionHistory;
}
break;
case "Completed":
listCount = model.collectionState2 == 'This Week'
? model.completedCollectionThisWeek.entries.length
: model.completedCollectionHistory.entries.length;
baseList = model.collectionState2 == "This Week"
? model.completedCollectionThisWeek
: model.completedCollectionHistory;
break;
case "Unavailable":
listCount = model.collectionState2 == 'This Week'
? model.unavailableCollectionThisWeek.entries.length
: model.unavailableCollectionHistory.entries.length;
baseList = model.collectionState2 == "This Week"
? model.unavailableCollectionThisWeek
: model.unavailableCollectionHistory;
break;
}
}
Widget renderUpcomingCollections() {
return PagedListView<int, UpcomingCollectionRequest>(
shrinkWrap: false,
pagingController: _upcomingCollectionsController,
builderDelegate: PagedChildBuilderDelegate<UpcomingCollectionRequest>(
itemBuilder: (context, item, index) {
return CollectionCard(
date: item.date!,
areaOfOperation: item.street!['areaOfOperation']['name'],
street:
'${item.street!['name']}, ${item.street!['area']}, ${item.street!['lga']['name']}, ${item.street!['state']['name']}',
streetId: item.street!['id'],
status: "missed",
daysLeft: item.daysLeft,
);
},
),
);
}
Widget renderCollectionsToday() {
return PagedListView<int, CollectionRequest>(
shrinkWrap: false,
pagingController: _collectionsTodayController,
builderDelegate: PagedChildBuilderDelegate<CollectionRequest>(
itemBuilder: (context, item, index) {
return CollectionCard(
date: item.createdAt!,
areaOfOperation: item.adminStreet!['areaOfOperation']['name'],
street:
'${item.adminStreet!['name']}, ${item.adminStreet!['area']}, ${item.adminStreet!['lga']['name']}, ${item.adminStreet!['state']['name']}',
streetId: item.id!,
status: "missed",
);
},
),
);
}
Widget renderWidget() {
switch (model.collectionState) {
default:
if (collectionListInUse == "upcomingList") {
return renderUpcomingCollections();
}
return renderCollectionsToday();
}
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 100.w,
decoration: BoxDecoration(
color: const Color(0xffE9EBFF),
borderRadius: BorderRadius.all(
Radius.circular(10.sp),
),
),
child: WasteNgLinearTab(
tabItems: const [
'Collections',
'Missed',
'Completed',
'Unavailable',
],
onItemSelected: (i, s) {
model.collectionState = s;
model.collectionState2 =
s != 'Collections' ? 'This Week' : 'Collections Today';
switch (s) {
case 'Collections':
// model.collectionStatusCount = model.collectionsToday.length;
break;
case 'Missed':
// to be revisited
// model.collectionStatusCount =
// model.missedCollectionReqs.length;
break;
}
},
value: model.collectionState,
).paddingSymmetric(horizontal: 0.w, vertical: 2.h),
).paddingSymmetric(horizontal: 5.w, vertical: 0.h),
WasteNgMaterialTab(
statusCount: const <int?>[null, null],
tabItems: model.collectionState == 'Missed' ||
model.collectionState == 'Completed' ||
model.collectionState == 'Unavailable'
? const ['This Week', 'History']
: const ['Collections Today', 'Upcoming Collections'],
onItemSelected: (i, s) {
model.collectionState2 = s;
},
value: model.collectionState2,
).paddingSymmetric(horizontal: 5.w, vertical: 1.h).paddingTop((1.5).h),
Expanded(
flex: 1,
child: listCount < 1
? const Center(child: Text("No Collection Requests"))
: renderWidget().paddingSymmetric(
horizontal: 5.w,
vertical: 0.h,
),
),
],
);
}
}
Thanks in advance ❤️
hi @Israel001, sorry for the delayed response. Did you manage to solve it?
i've tried to add customscrollview but it's still loading even i'm not doing scrolling or load more action on the screen can anyone help me please i've been stuck for so long