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

Flutter分页新数据未追加至ListView.builder尾部,替换旧数据

Flutter 分页加载:解决ListView.builder替换数据而非追加的问题

我有一个API,每次请求可获取10条数据条目,期望将这些新条目追加到ListView.builder的列表尾部,但目前ListView.builder会刷新列表、替换原有数据,无法实现加载更多功能。以下是问题根源、修复方案及完整代码:

问题根源

  • 未维护全局数据列表:每次API请求返回的数据直接用于渲染,没有和原有数据合并
  • FutureBuilder使用不当:FutureBuilder的future参数直接绑定动态变化的getDiscussion(offSetCurrent),导致offset更新时重新发起请求并替换全部数据
  • 加载状态未正确控制:缺少isLoading状态的有效使用,可能引发重复请求

修复方案

1. 新增全局数据存储

在State类中添加一个列表,用于保存所有已加载的讨论消息:

List<DiscussionsMessages> allMessages = [];

2. 修改API请求逻辑

调整getDiscussion方法,将新获取的数据追加到全局列表,并通过setState更新UI:

Future<void> getDiscussion(int offSetCurrent) async {
  if (isLoading || !hasMore) return;
  setState(() => isLoading = true);

  SharedPreferences sp = await SharedPreferences.getInstance();
  var parameters = {
    'RangeFrom': offSetCurrent.toString(),
    'AccessToken': sp.getString('AccessToken'),
    'DiscussionID': '22848',
  };
  var urlfetch = Uri(
    scheme: "https",
    host: baseUrl,
    path: discussionAPIPath,
    queryParameters: parameters,
  );
  try {
    http.Response response = await http.get(urlfetch);
    var data = jsonDecode(response.body.toString());
    var snapshot = PaginatedModel.fromJson(data);
    var newMessages = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages ?? [];
    
    // 将新数据追加到全局列表
    setState(() {
      allMessages.addAll(newMessages);
      hasMore = newMessages.length == 10;
      isLoading = false;
    });

    print('Discussion API HIT with offset $offSetCurrent, loaded ${newMessages.length} items');
  } catch (e) {
    print(e.toString());
    setState(() => isLoading = false);
  }
}

3. 重构UI渲染逻辑

移除FutureBuilder,直接基于全局列表allMessages构建ListView,并处理加载状态:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: allMessages.isEmpty 
        ? const Center(child: CircularProgressIndicator())
        : ListView.builder(
            controller: scrollController,
            itemCount: allMessages.length + (hasMore ? 1 : 0),
            itemBuilder: (context, idx) {
              if (idx < allMessages.length) {
                return Container(
                  margin: const EdgeInsets.all(10),
                  child: Html(data: allMessages[idx].message.toString()),
                );
              } else {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 32),
                  child: Center(
                    child: isLoading 
                        ? const CircularProgressIndicator() 
                        : const Text('No More Data'),
                  ),
                );
              }
            },
          ),
  );
}

4. 完善初始化与滚动监听

调整initState,初始化加载第一页数据,并修正滚动监听逻辑:

@override
void initState() {
  super.initState();
  // 初始化加载第一页
  getDiscussion(offSetCurrent);
  
  scrollController.addListener(() {
    // 滚动到接近底部时触发加载(提前10%触发,提升体验)
    if (scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.9 && !isLoading && hasMore) {
      setState(() {
        offSetCurrent += 10;
        getDiscussion(offSetCurrent);
      });
    }
  });
}

修改后的完整代码

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

  @override
  State<PaginatedScreen> createState() => _PaginatedScreenState();
}

class _PaginatedScreenState extends State<PaginatedScreen> {
  final scrollController = ScrollController();
  int offSetCurrent = 0;
  bool hasMore = true;
  bool isLoading = false;
  // 全局存储所有已加载的消息
  List<DiscussionsMessages> allMessages = [];

  @override
  void initState() {
    super.initState();
    // 初始化加载第一页数据
    getDiscussion(offSetCurrent);
    
    scrollController.addListener(() {
      // 滚动到接近底部时触发加载(提前10%触发,提升体验)
      if (scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.9 && !isLoading && hasMore) {
        setState(() {
          offSetCurrent += 10;
          getDiscussion(offSetCurrent);
        });
      }
    });
  }

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

  Future<void> getDiscussion(int offSetCurrent) async {
    if (isLoading || !hasMore) return;
    setState(() => isLoading = true);

    SharedPreferences sp = await SharedPreferences.getInstance();
    var parameters = {
      'RangeFrom': offSetCurrent.toString(),
      'AccessToken': sp.getString('AccessToken'),
      'DiscussionID': '22848',
    };
    var urlfetch = Uri(
      scheme: "https",
      host: baseUrl,
      path: discussionAPIPath,
      queryParameters: parameters,
    );
    try {
      http.Response response = await http.get(urlfetch);
      var data = jsonDecode(response.body.toString());
      var snapshot = PaginatedModel.fromJson(data);
      var newMessages = snapshot.courseLessonDetailsData!.first.discussionDetails!.first.discussionsMessages ?? [];
      
      setState(() {
        allMessages.addAll(newMessages);
        // 如果返回数据不足10条,说明没有更多数据了
        hasMore = newMessages.length == 10;
        isLoading = false;
      });

      print('Discussion API HIT with offset $offSetCurrent, loaded ${newMessages.length} items');
    } catch (e) {
      print(e.toString());
      setState(() => isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: allMessages.isEmpty 
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              controller: scrollController,
              itemCount: allMessages.length + (hasMore ? 1 : 0),
              itemBuilder: (context, idx) {
                if (idx < allMessages.length) {
                  return Container(
                    margin: const EdgeInsets.all(10),
                    child: Html(data: allMessages[idx].message.toString()),
                  );
                } else {
                  return Padding(
                    padding: const EdgeInsets.symmetric(vertical: 32),
                    child: Center(
                      child: isLoading 
                          ? const CircularProgressIndicator() 
                          : const Text('No More Data'),
                    ),
                  );
                }
              },
            ),
    );
  }
}

关键说明

  • 全局数据列表:allMessages负责存储所有已加载的条目,每次新数据都追加到列表末尾
  • 加载状态控制:isLoading防止同一时间发起多个请求,hasMore判断是否还有更多数据可加载
  • 滚动监听优化:提前10%触发加载,避免用户等待,提升交互体验
  • 移除FutureBuilder:直接基于全局状态渲染UI,避免每次请求替换全部数据

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 20:10:25