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
相关产品推荐
相关产品推荐

