Flutter列表滚动到底部时触底分页加载失效问题
问题原因
分页不触发、数据不加载是几个低级错误叠加导致的:
- 滚动控制器绑定完全错误。页面套了两层无用的
SingleChildScrollView,你初始化的controller传给了内部的ListViewSearch,但多层嵌套滚动会导致外层拦截滚动事件,controller根本监听不到列表的真实滚动位置,extentAfter的值永远不对,触底判断自然不会生效。 - 监听器移除逻辑失效。
dispose时传了个全新的匿名函数去移除监听,和初始化时绑定的不是同一个回调,不仅会造成内存泄漏,极端场景下还会触发回调异常。 - 分页参数逻辑错误。你每次加载更多只给接口传累加的
limit,不传偏移量/页码参数,接口每次返回的都是前N条数据,就算触发加载,addAll之后也会全是重复数据。 - 缺少防重复触发判断。触底条件没加
isLoadMoreRunning == false校验,滚动时会连续触发多次请求,甚至直接打挂接口。 - 列表没有被
Expanded包裹。ListViewSearch放在Column里没有撑开高度,会出现滚动边界异常,甚至根本不会产生滚动。
修复步骤
- 清理冗余滚动嵌套,正确绑定控制器
删掉两层多余的SingleChildScrollView,给列表组件加Expanded撑满可用空间,让列表自身管理滚动,不要在可滚动组件里再套可滚动组件。
修正后的布局代码:
注意检查Widget _child(Size size, double paddingTop, StationCubit stationCubit) => BlocBuilder<StationCubit, StationState>( builder: (context, state) => Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column( children: [ const SizedBox(height: 17), Expanded( child: _addresses(size, stationCubit), ), ], ), ), ); Widget _addresses(Size size, StationCubit stationCubit) => ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height / 2, ), child: SizedBox( width: size.width, child: ClipRRect( borderRadius: BorderRadius.circular(24), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ isFirstLoadRunning ? const CircularProgressIndicator(color: Colors.white) : const Text('Addresses', style: constants.Styles.smallBookTextStyleWhite), const SizedBox(height: 25), Expanded( child: ListViewSearch( stationList: stationsList, controller: controller, ), ), const SizedBox(height: 20), if (isLoadMoreRunning) const Padding( padding: EdgeInsets.only(top: 10, bottom: 40), child: Center(child: CircularProgressIndicator()), ), if (!hasNextStation) Container( padding: const EdgeInsets.only(top: 30, bottom: 40), color: Colors.amber, child: const Center(child: Text('You have fetched all of the content')), ), ], ), ), ), ), ), );ListViewSearch内部实现,必须使用外部传入的controller绑定到它内部的ListView/CustomScrollView上,不要在组件内部自己创建controller。 - 修正监听器生命周期逻辑
不要用匿名函数绑定监听,保证添加和移除的是同一个方法,同时记得释放controller,initState里的super调用要放最前面:@override void initState() { super.initState(); _firstLoadStations(); controller = ScrollController()..addListener(_loadMoreStations); _foundAddresses = _allAddresses; _foundStation = _allStation; } @override void dispose() { controller.removeListener(_loadMoreStations); controller.dispose(); super.dispose(); } - 修正分页参数和防重逻辑
固定每页条数为10,新增偏移量参数,不要累加limit。常规分页接口要么传page+pageSize,要么传offset+limit,只传累加的limit等于每次要前N条,永远拿不到后面的数据。同时补全加载状态判断,防止重复请求:
同步修改Cubit和Repository层的方法,新增offset参数透传给接口即可。final int pageSize = 10; int offset = 0; void _firstLoadStations() async { final StationCubit stationCubit = BlocProvider.of<StationCubit>(context); setState(() => isFirstLoadRunning = true); try { stationsList = await stationCubit.getAllPublicChargingStations( limit: pageSize, offset: offset, ); } catch (error) { debugPrint(error.toString()); } setState(() => isFirstLoadRunning = false); } void _loadMoreStations() async { final StationCubit stationCubit = BlocProvider.of<StationCubit>(context); // 补全加载中判断,防止重复触发 if (hasNextStation && !isFirstLoadRunning && !isLoadMoreRunning && controller.position.extentAfter < 30) { setState(() => isLoadMoreRunning = true); offset += pageSize; try { var fetchedStations = await stationCubit.getAllPublicChargingStations( limit: pageSize, offset: offset, ); if (fetchedStations.isNotEmpty) { setState(() => stationsList.addAll(fetchedStations)); // 返回条数小于每页条数,说明已经到最后一页 if (fetchedStations.length < pageSize) { hasNextStation = false; } } else { setState(() => hasNextStation = false); } } catch (error) { debugPrint(error.toString()); // 请求失败回退偏移量,避免下次加载跳过数据 offset -= pageSize; } setState(() => isLoadMoreRunning = false); } }
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

