Flutter使用ListView.Builder下拉分页时出现重复条目问题排查
问题核心原因
- BlocBuilder的builder方法存在副作用
builder是纯渲染函数,Flutter会在父组件重绘、状态更新等多种场景下触发多次执行。你当前每次触发build且状态为RecommendedViewAllStateSuccess时,都会把当前分页数据重复追加到页面本地的recommended列表中,直接导致数据重复。 - 滚动监听重复注册
你把scrollController.addListener逻辑写在了ListView的controller赋值逻辑中,每次build都会给同一个ScrollController新增一个监听器。滚动到底部时多个监听器同时触发,会多次发送加载下一页的事件,同一页码被重复请求多次,进一步导致数据重复插入。 - 数据存储逻辑不符合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
相关产品推荐
相关产品推荐

