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

Flutter使用graphql_flutter在ListView.builder实现服务端无限滚动加载

graphql_flutter 实现ListView无限滚动加载方案

你可以通过以下修改完成无限滚动功能,完整可运行代码如下:

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:cached_network_image/cached_network_image.dart';

class MoreEpd extends StatefulWidget {
  final String moreId;
  const MoreEpd({Key? key, required this.moreId}) : super(key: key);

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

class _MoreEpdState extends State<MoreEpd> {
  double pageWidth = 0;
  double pageHeight = 0;
  int pageNum = 0;
  // 存储全量列表数据
  List allDetails = [];
  // 分页加载中状态
  bool isLoadingMore = false;
  // 是否还有更多数据
  bool hasMore = true;
  // 滚动控制器监听滚动位置
  final ScrollController _scrollController = ScrollController();

  final String leftArrow = 'assets/icons/left-arrow.svg';
  String getSearchResult = """
 query homeview(\$moreId: ID!, \$page: Int! ){
    homeview(HM_ID: \$moreId, page: \$page){
    HM_ID
    HM_Type_ID
    HM_Type_Name
    HM_NAME
    Priority
    Details{
      HM_Det_ID
      HM_ID
      Ep_ID
      Pod_ID
      Link
      Image
      title
      Pod_title
      
    }
  }
}
""";

  @override
  void initState() {
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    pageWidth = MediaQuery.of(context).size.width;
    pageHeight = MediaQuery.of(context).size.height;
    return Container(
      child: Query(
        options: QueryOptions(
          document: gql(getSearchResult),
          variables: {'moreId': widget.moreId, 'page': pageNum},
        ),
        builder: (
          QueryResult result, {
          Refetch? refetch,
          FetchMore? fetchMore,
        }) {
          // 处理首次加载状态
          if (result.isLoading && allDetails.isEmpty) {
            return const Center(child: CircularProgressIndicator());
          }
          // 处理请求错误
          if (result.hasException) {
            return Center(child: Text('加载失败:${result.exception.toString()}'));
          }
          // 首次加载成功后存储第一页数据
          if (result.data != null && allDetails.isEmpty) {
            final firstPageData = result.data!['homeview']['Details'] ?? [];
            allDetails = List.from(firstPageData);
            // 如果第一页就返回空,标记无更多数据
            if (firstPageData.isEmpty) {
              hasMore = false;
            }
          }

          // 加载下一页逻辑
          void loadMore() async {
            if (fetchMore == null) return;
            final fetchMoreOptions = FetchMoreOptions(
              variables: {'moreId': widget.moreId, 'page': pageNum + 1},
              updateQuery: (previous, current) {
                if (current == null) return previous;
                final newDetails = current['homeview']['Details'] ?? [];
                // 新一页返回空,说明无更多数据
                if (newDetails.isEmpty) {
                  setState(() {
                    hasMore = false;
                    isLoadingMore = false;
                  });
                  return previous;
                }
                // 合并新旧数据
                final mergedData = Map<String, dynamic>.from(previous!);
                mergedData['homeview']['Details'] = [
                  ...mergedData['homeview']['Details'],
                  ...newDetails
                ];
                setState(() {
                  pageNum += 1;
                  isLoadingMore = false;
                  allDetails = List.from(mergedData['homeview']['Details']);
                });
                return mergedData;
              },
            );
            await fetchMore(fetchMoreOptions);
          }

          // 绑定滚动监听
          if (!isLoadingMore && hasMore && _scrollController.hasClients) {
            _scrollController.addListener(() {
              if (isLoadingMore || !hasMore) return;
              // 距离底部不足200px时触发加载,可根据业务调整阈值
              if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
                setState(() {
                  isLoadingMore = true;
                });
                loadMore();
              }
            });
          }

          return handleResult(result);
        },
      ),
    );
  }

  Widget handleResult(QueryResult result) {
    return ListView.builder(
      controller: _scrollController,
      padding: const EdgeInsets.only(top: 15),
      // 额外加1位显示底部加载/无更多提示
      itemCount: allDetails.length + 1,
      itemBuilder: (context, index) {
        // 最后一项处理状态提示
        if (index == allDetails.length) {
          if (!hasMore) {
            return const Padding(
              padding: EdgeInsets.symmetric(vertical: 15),
              child: Center(child: Text('没有更多数据了')),
            );
          }
          return isLoadingMore
              ? const Padding(
                  padding: EdgeInsets.symmetric(vertical: 15),
                  child: Center(child: CircularProgressIndicator()),
                )
              : const SizedBox.shrink();
        }
        final item = allDetails[index];
        return InkWell(
          onTap: () {},
          child: Padding(
            padding: EdgeInsets.only(
                right: pageWidth * 0.08,
                left: pageWidth * 0.08,
                bottom: pageWidth * 0.06),
            child: Container(
              child: Stack(
                children: [
                  Column(
                    children: [
                      Row(
                        children: [
                          Padding(
                            padding: EdgeInsets.only(left: pageWidth * 0.01),
                            child: Container(
                              width: pageWidth * 0.128,
                              height: pageWidth * 0.128,
                              decoration: BoxDecoration(
                                  image: DecorationImage(
                                      fit: BoxFit.cover,
                                      image: CachedNetworkImageProvider(
                                        item['Image'],
                                      )),
                                  borderRadius: const BorderRadius.all(Radius.circular(15)),
                                  border: Border.all(
                                    color: Colors.grey, // 可替换为你自定义的MyColors.lightGrey
                                    width: 1,
                                  )),
                            ),
                          ),
                          Expanded(
                            child: Row(
                              children: [
                                Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: [
                                    Container(
                                      width: pageWidth * 0.5,
                                      alignment: Alignment.centerRight,
                                      child: Text(
                                        item['title'],
                                        textAlign: TextAlign.right,
                                        overflow: TextOverflow.ellipsis,
                                        maxLines: 1,
                                        style: const TextStyle(
                                          fontSize: 14,
                                        ),
                                      ),
                                    ),
                                  ],
                                ),
                              ],
                            ),
                          )
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

核心修改说明

  • 新增allDetails变量存储全量列表数据,避免分页请求后覆盖旧数据
  • 新增isLoadingMore、hasMore状态控制分页逻辑,避免重复请求和无数据时的无效请求
  • 通过ScrollController监听滚动位置,接近底部时自动触发下一页加载
  • 利用graphql_flutter自带的FetchMore方法发起分页请求,通过updateQuery回调合并新旧数据
  • 列表底部新增加载指示器和无数据提示,补充首次加载、请求错误的状态处理

注意事项

  • 可根据实际业务调整触发加载的底部距离阈值
  • 如果moreId参数会动态变化,需要新增didUpdateWidget生命周期方法,在moreId变更时重置pageNum、allDetails、hasMore等状态,重新加载第一页数据
  • 如果列表嵌套在其他滚动组件中,需要开启shrinkWrap: true,非嵌套场景建议关闭该属性避免性能损耗
  • 代码中的颜色、资源路径等配置可替换为你项目内的实际定义

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 23:36:02