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

Flutter列表滚动到底部时触底分页加载失效问题

问题原因

分页不触发、数据不加载是几个低级错误叠加导致的:

  • 滚动控制器绑定完全错误。页面套了两层无用的SingleChildScrollView,你初始化的controller传给了内部的ListViewSearch,但多层嵌套滚动会导致外层拦截滚动事件,controller根本监听不到列表的真实滚动位置,extentAfter的值永远不对,触底判断自然不会生效。
  • 监听器移除逻辑失效。dispose时传了个全新的匿名函数去移除监听,和初始化时绑定的不是同一个回调,不仅会造成内存泄漏,极端场景下还会触发回调异常。
  • 分页参数逻辑错误。你每次加载更多只给接口传累加的limit,不传偏移量/页码参数,接口每次返回的都是前N条数据,就算触发加载,addAll之后也会全是重复数据。
  • 缺少防重复触发判断。触底条件没加isLoadMoreRunning == false校验,滚动时会连续触发多次请求,甚至直接打挂接口。
  • 列表没有被Expanded包裹。ListViewSearch放在Column里没有撑开高度,会出现滚动边界异常,甚至根本不会产生滚动。
修复步骤
  1. 清理冗余滚动嵌套,正确绑定控制器
    删掉两层多余的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。
  2. 修正监听器生命周期逻辑
    不要用匿名函数绑定监听,保证添加和移除的是同一个方法,同时记得释放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();
    }
    
  3. 修正分页参数和防重逻辑
    固定每页条数为10,新增偏移量参数,不要累加limit。常规分页接口要么传page+pageSize,要么传offset+limit,只传累加的limit等于每次要前N条,永远拿不到后面的数据。同时补全加载状态判断,防止重复请求:
    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);
      }
    }
    
    同步修改Cubit和Repository层的方法,新增offset参数透传给接口即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:09:22