You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Firestore StreamBuilder无限分页:初始项重复显示问题

问题:Firestore分页初始加载数据重复异常

我参考Stack Overflow的方案实现了基于Firestore StreamBuilder的分页功能,修改数据项时表现正常,但添加DocumentChangeType.added的处理逻辑后,出现初始加载时所有项重复显示的异常。以下是调整后的代码:

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],
        ),
      );
    },
  );
}

我尝试了多种方法仍无法定位问题原因,恳请各位提供帮助,如需更多信息请告知!

内容的提问来源于stack exchange,提问作者Chris

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 10:47:03