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

Flutter ScrollController懒加载 数据量少时无法触发加载问题

问题根源

现有懒加载逻辑的触发条件是「滚动位置等于列表最大滚动距离」,该逻辑仅在列表可滚动时生效。当初始加载的列表项总高度小于ListView视口高度时,列表无滚动空间,maxScrollExtent值为0,滚动事件不会触发,懒加载逻辑自然无法执行。

解决方案

以下两种方案都可以解决该问题,可根据项目实际情况选择:

方案1:保留原有ScrollController逻辑,新增主动检查机制

改造成本最低,不需要重构原有滚动监听代码,核心逻辑是在首帧渲染完成、每次加载完新数据后,主动检查当前列表是否已经撑满视口,未撑满则自动触发加载,直到列表可滚动或无更多数据为止。

核心修改点:

  • 新增加载状态标记,避免重复触发请求
  • 新增视口检查方法,首帧、每次数据加载完成后调用
  • 将原有滚动监听的相等判断改为大于等于判断,兼容不同设备滚动精度问题
  • 补充dispose逻辑释放控制器资源

修改后的完整可运行代码:

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static const String _title = 'Flutter 懒加载示例';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _title,
      home: Scaffold(
        appBar: AppBar(title: const Text(_title)),
        body: const Center(
          child: MyStatefulWidget(),
        ),
      ),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  late List myList;
  final ScrollController _scrollController = ScrollController();
  final int _initialItemsLength = 1;
  int _currentMax = 0;
  // 加载锁,防止重复触发请求
  bool _isLoading = false;
  // 标记是否还有更多数据,实际业务由接口返回值控制
  bool _hasMore = true;
  // 单页加载数量
  final int _pageSize = 10;

  @override
  void initState() {
    super.initState();
    myList = List.generate(_initialItemsLength, (i) => "Item : ${i + 1}");
    _currentMax = _initialItemsLength;

    _scrollController.addListener(() {
      // 改为大于等于判断,避免滚动精度问题导致漏触发
      if (_scrollController.position.pixels >=
          _scrollController.position.maxScrollExtent) {
        _loadMoreData();
      }
    });
    // 首帧渲染完成后检查是否需要加载更多
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _checkIfNeedLoadMore();
    });
  }

  // 检查列表是否撑满视口,未撑满则自动加载
  void _checkIfNeedLoadMore() {
    if (!_hasMore || _isLoading || !_scrollController.hasClients) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.position.pixels >=
          _scrollController.position.maxScrollExtent) {
        _loadMoreData();
      }
    });
  }

  Future _loadMoreData() async {
    if (_isLoading || !_hasMore) return;
    _isLoading = true;
    // 模拟接口请求延迟
    await Future.delayed(const Duration(milliseconds: 300));
    for (int i = _currentMax; i < _currentMax + _pageSize; i++) {
      myList.add("Item : ${i + 1}");
      // 实际业务中如果接口返回的列表长度小于_pageSize,说明没有更多数据,将_hasMore设为false即可终止加载
    }
    _currentMax += _pageSize;
    _isLoading = false;
    if (mounted) setState(() {});
    // 加载完成后再次检查,若仍未撑满视口则继续加载
    _checkIfNeedLoadMore();
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        controller: _scrollController,
        itemBuilder: (context, i) {
          if (i == myList.length) {
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: Center(
                child: _hasMore
                    ? const CupertinoActivityIndicator()
                    : const Text("没有更多数据了"),
              ),
            );
          }
          return ListTile(
            title: Text(myList[i]),
          );
        },
        itemCount: myList.length + 1,
      ),
    );
  }
}

方案2:使用ScrollNotification监听滚动状态

不需要依赖ScrollController,通过NotificationListener监听列表滚动通知,初始状态下也能正确获取视口尺寸判断是否需要加载,还可以灵活设置预加载阈值,提升列表滑动流畅度。
核心实现是给ListView外层包裹NotificationListener<ScrollNotification>,在滚动回调中判断当前位置距离底部的距离,小于阈值时触发加载:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: NotificationListener<ScrollNotification>(
      onNotification: (notification) {
        final metrics = notification.metrics;
        // 距离底部不足200像素时预加载,可根据业务调整阈值
        if (metrics.pixels >= metrics.maxScrollExtent - 200) {
          _loadMoreData();
        }
        return false;
      },
      child: ListView.builder(
        itemBuilder: (context, i) {
          if (i == myList.length) {
            return Padding(
              padding: const EdgeInsets.symmetric(vertical: 16),
              child: Center(
                child: _hasMore
                    ? const CupertinoActivityIndicator()
                    : const Text("没有更多数据了"),
              ),
            );
          }
          return ListTile(
            title: Text(myList[i]),
          );
        },
        itemCount: myList.length + 1,
      ),
    ),
  );
}

该方案同样需要配合方案1中的_isLoading加载锁、_hasMore结束标记、首帧检查逻辑使用,避免重复请求和死循环。

注意事项
  • 必须加加载锁,否则短时间内多次触发加载条件会发起重复请求
  • 必须维护「是否还有更多数据」的标记,数据全部加载完成后终止加载逻辑,避免无限触发
  • 不要在build方法执行过程中直接获取ScrollController的position信息,会抛出布局未完成的异常,需要放在addPostFrameCallback回调中获取
  • 建议设置预加载阈值(比如距离底部200-500像素就开始加载),比滚动到底才加载的体验更流畅
  • 使用ScrollController时必须在dispose生命周期中释放资源,避免内存泄漏

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 01:21:41