I followed this SO-Answer on how to implement pagination
with a Firestore StreamBuilder
. When an item is modified, it is working fine. However I added another case for DocumentChangeType.added
but that results in a weird behavior because all initial items are displayed twice.
This is my adjusted code:
final StreamController<List<DocumentSnapshot>> _streamController =
StreamController<List<DocumentSnapshot>>();
final List<DocumentSnapshot> _notifications = [];
bool _isRequesting = false;
bool _isFinish = false;
final CollectionReference<Map<String, dynamic>> _notificationCollectionRef =
FirebaseFirestore.instance
.collection(BackendKeys.user.allUsers)
.doc(AuthService.currentUser!.uid)
.collection(BackendKeys.notifications.notifications);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (!AuthService.currentUser!.isAnonymous) {
unawaited(PushNotificationService.initIfPermissionIsGranted(
requestPermission: true,
));
}
});
_notificationCollectionRef.snapshots().listen((data) {
print('change in listner');
onChangeData(data.docChanges);
});
requestNextPage();
}
@override
void dispose() {
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: sidePadding,
),
child: SafeArea(
child: Column(
children: [
const SizedBox(
height: sidePadding,
),
// Wrapping in SizedBox with height of buttons so
// the header is lined up with the other views (friends & settings)
SizedBox(
height: touchTargetForButtons,
child: Row(
children: [
Text(
'Inbox'.tr,
style: AppTextStyles.avenirNextH2Bold.copyWith(
color: white,
),
),
const SizedBox(
width: 20,
),
],
),
),
const SizedBox(
height: sidePadding,
),
Flexible(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollInfo) {
if (scrollInfo.metrics.maxScrollExtent ==
scrollInfo.metrics.pixels) {
requestNextPage();
}
return true;
},
child: StreamBuilder(
stream: _streamController.stream,
builder: (context, snapshot) {
return AnimatedSwitcher(
duration: kThemeAnimationDuration,
child:
snapshot.connectionState == ConnectionState.waiting
? _buildLoadingView()
: snapshot.data == null
? const ErrorView()
: _buildNotifications(
List.generate(
snapshot.data!.length,
(index) => PushNotification.fromJson(
snapshot.data![index].data()
as Map<String, dynamic>,
),
),
),
);
},
),
),
),
],
),
),
),
);
}
void onChangeData(List<DocumentChange> documentChanges) {
for (var notificationChange in documentChanges) {
switch (notificationChange.type) {
case DocumentChangeType.added:
_notifications.add(notificationChange.doc);
break;
case DocumentChangeType.modified:
int indexWhere = _notifications.indexWhere((notification) {
return notificationChange.doc.id == notification.id;
});
if (indexWhere >= 0) {
_notifications[indexWhere] = notificationChange.doc;
}
break;
case DocumentChangeType.removed:
_notifications.removeWhere((notification) {
return notificationChange.doc.id == notification.id;
});
break;
}
}
_streamController.add(_notifications);
}
void requestNextPage() async {
if (!_isRequesting && !_isFinish) {
QuerySnapshot querySnapshot;
_isRequesting = true;
if (_notifications.isEmpty) {
querySnapshot = await _notificationCollectionRef
.orderBy(
BackendKeys
.notifications.sentAtDateTimeSinceMillisecondsSinceEpoch,
)
.limit(10)
.get();
} else {
querySnapshot = await _notificationCollectionRef
.orderBy(
BackendKeys
.notifications.sentAtDateTimeSinceMillisecondsSinceEpoch,
)
.startAfterDocument(_notifications[_notifications.length - 1])
.limit(10)
.get();
}
int oldSize = _notifications.length;
print('oldSize: ' + oldSize.toString());
_notifications.addAll(querySnapshot.docs);
int newSize = _notifications.length;
if (oldSize != newSize) {
_streamController.add(_notifications);
} else {
_isFinish = true;
}
_isRequesting = false;
print(newSize);
}
}
_buildNotifications(List<PushNotification> notifications) {
if (notifications.isEmpty) {
return _buildEmptyNotificationsView();
}
notifications.sort(
(a, b) => b.sentAtDateTimeSinceMillisecondsSinceEpoch.compareTo(
a.sentAtDateTimeSinceMillisecondsSinceEpoch,
),
);
return ListView.builder(
key: const Key('non_empty_notifications'),
itemCount: notifications.length + 1,
itemBuilder: (context, index) {
if (index == notifications.length) {
return SizedBox(
height: safeAreaBottom + bottomNavBarHeight + sidePadding,
);
}
return Padding(
padding: const EdgeInsets.only(
bottom: 20,
),
child: NotificationTile(
key: UniqueKey(),
notification: notifications[index],
),
);
},
);
}
I tried a lot but simply can not figure out why on the initial load, my items are displayed twice... Grateful for every help here! Let me know if you need any more info!