0

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!

Chris
  • 1,828
  • 6
  • 40
  • 108
  • When you add the initial items to `_notifications` in the `onChangeData` method and then call `_streamController.add(_notifications)`, the StreamBuilder receives a new list instance with the initial items. As a result, the `StreamBuilder` rebuilds and displays the initial items again. So initialize `_notifications` instance variable inside `initState` state. And make corresponding changes in switch case's `DocumentChangeType.added` case. – Rohit Kharche Jun 29 '23 at 06:59
  • @RohitKharche how would the changes look like? – Chris Jun 29 '23 at 07:37

0 Answers0