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

Flutter使用ListView.Builder下拉分页时出现重复条目问题排查

问题核心原因

  1. BlocBuilder的builder方法存在副作用
    builder是纯渲染函数,Flutter会在父组件重绘、状态更新等多种场景下触发多次执行。你当前每次触发build且状态为RecommendedViewAllStateSuccess时,都会把当前分页数据重复追加到页面本地的recommended列表中,直接导致数据重复。
  2. 滚动监听重复注册
    你把scrollController.addListener逻辑写在了ListView的controller赋值逻辑中,每次build都会给同一个ScrollController新增一个监听器。滚动到底部时多个监听器同时触发,会多次发送加载下一页的事件,同一页码被重复请求多次,进一步导致数据重复插入。
  3. 数据存储逻辑不符合Bloc规范
    分页全量列表数据应该存储在Bloc层而非页面State中,页面只负责根据Bloc状态渲染,避免页面侧维护数据引发的一致性问题。

修复方案

1. 调整Bloc代码,内部维护全量分页数据

class RecommendedViewAllBloc
    extends Bloc<RecommendedViewAllEvent, RecommendedViewAllState> {
  RecommendedForYouDataService dataService;
  int page = 0;
  bool isFetching = false;
  // Bloc内部维护全量列表数据
  List<RecommendedForYouData> fullList = [];
  RecommendedViewAllBloc(this.dataService)
      : super(RecommendedViewAllStateInitial());

  @override
  Stream<RecommendedViewAllState> mapEventToState(
    RecommendedViewAllEvent event,
  ) async* {
    yield RecommendedViewAllStateLoading();
    try {
      List<RecommendedForYouData> newPageData = 
          await dataService.makeRequestRecommendedForYouViewAll(page)
              as List<RecommendedForYouData>;
      // 新页数据追加到Bloc内部的全量列表
      fullList.addAll(newPageData);
      // 返回全量列表给渲染层
      yield RecommendedViewAllStateSuccess(List.from(fullList));
      page++;
    } catch (e) {
      yield RecommendedViewAllStateFailure(e.toString());
    }
  }
}

2. 调整页面代码,移除本地数据维护逻辑、滚动监听移到初始化阶段

class _RecommendedForYouPageState extends State<RecommendedForYouPage> {
  late RecommendedViewAllBloc recommendedViewAllBloc;
  late final ScrollController scrollController = ScrollController();
  // 移除页面侧维护的recommended列表

  @override
  void initState() {
    super.initState();
    recommendedViewAllBloc = BlocProvider.of<RecommendedViewAllBloc>(context);
    // 滚动监听移到initState,全局只注册一次
    scrollController.addListener(() {
      if (scrollController.position.atEdge && !recommendedViewAllBloc.isFetching) {
        if (scrollController.position.pixels != 0) {
          recommendedViewAllBloc
            ..isFetching = true
            ..add(LoadRestaurantRecommendedViewAllEvent());
        }
      }
    });
    recommendedViewAllBloc.add(LoadRestaurantRecommendedViewAllEvent());
  }

  @override
  Widget build(BuildContext context) {
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;
    return Scaffold(
        body: Container(
          transform: Matrix4.translationValues(0, -1, 0),
          color: TuxedoColor.redColor,
          child: Padding(
            padding: const EdgeInsets.only(top: 1.0),
            child: Container(
              decoration: BoxDecoration(
                  color: TuxedoColor.bodyColor,
                  borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(40),
                    topRight: Radius.circular(40),
                  )),
              child: Padding(
                padding:
                    const EdgeInsets.only(top: 20.0, left: 20.0, right: 20.0),
                child: BlocBuilder<RecommendedViewAllBloc,
                    RecommendedViewAllState>(
                  builder: (context, state) {
                    // 移除所有修改数据的副作用逻辑,仅做状态判断
                    if (state is RecommendedViewAllStateInitial ||
                        state is RecommendedViewAllStateLoading &&
                            recommendedViewAllBloc.fullList.isEmpty) {
                      return const Center(child: CircularProgressIndicator());
                    } else if (state is RecommendedViewAllStateFailure &&
                        recommendedViewAllBloc.fullList.isEmpty) {
                      return Center(child: Text(state.message));
                    }
                    // 直接从状态/Bloc中取全量列表渲染
                    final listData = state is RecommendedViewAllStateSuccess 
                        ? state.data 
                        : recommendedViewAllBloc.fullList;
                    return ListView.builder(
                      controller: scrollController,
                      itemBuilder: (context, index) => recommendedListItem(
                        listData[index],
                      ),
                      itemCount: listData.length,
                      shrinkWrap: true,
                      primary: false,
                      scrollDirection: Axis.vertical,
                    );
                  },
                ),
              ),
            ),
          ),
        ));
  }

  // 原有recommendedListItem、dispose方法保持不变
}

可选优化

建议将isFetching也作为Bloc状态的一部分维护,不要直接修改Bloc的实例属性,更符合Bloc单向数据流的设计规范。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 11:27:02