如何为Flutter FutureBuilder的GridView.builder添加无限滚动功能
Flutter 无限滚动分页加载实现修正
现有代码核心问题
- 接口分页参数错误:请求URL中仅传递了页码值,缺少
page参数名,后端无法识别分页信息 - 没有维护全局列表数据,每次分页请求的结果没有和已有数据合并,无法实现滚动累加效果
- 滚动监听没有做加载状态拦截,快速滚动到底部会重复触发多次请求
- 缺少无更多数据的判断逻辑和对应提示布局
- FutureBuilder默认每次页面重绘都会重新触发接口请求,不符合首次加载逻辑
修改后完整代码
import 'package:flutter/material.dart'; import 'package:test/model/recipe.dart'; import 'package:test/pages/recipe_details.dart'; import 'package:test/widgets/recipe_card.dart'; import 'package:http/http.dart' as http; class SingleCategory extends StatefulWidget { final Recipe recipe; final int recipeCourseid; final String recipeCourseName; SingleCategory({ this.recipe, @required this.recipeCourseid, @required this.recipeCourseName, }); @override _SingleCategoryState createState() => _SingleCategoryState(); } class _SingleCategoryState extends State<SingleCategory> { ScrollController _scrollController = ScrollController(); int pageNumber = 1; List<Recipe> _allRecipes = []; // 存储所有加载完成的食谱数据 bool _isLoading = false; // 加载状态标记,防止重复请求 bool _hasMore = true; // 是否还有更多可加载数据 final int _pageSize = 10; // 每页加载数量 Future<void> getRecipeList() async { if (_isLoading || !_hasMore) return; setState(() { _isLoading = true; }); try { var response = await http.get( 'https://test.com/wp-json/wp/v2/wprm_recipe?wprm_course=${widget.recipeCourseid}&per_page=$_pageSize&page=$pageNumber'); var body = response.body; final List<Recipe> recipes = recipeFromJson(body); setState(() { if (pageNumber == 1) { _allRecipes = recipes; } else { _allRecipes.addAll(recipes); } // 返回数据长度小于页长说明没有更多数据了 if (recipes.length < _pageSize) { _hasMore = false; } }); } catch (e) { print('加载失败:$e'); } finally { setState(() { _isLoading = false; }); } } @override void initState() { super.initState(); // 首次加载数据 getRecipeList(); _scrollController.addListener(() { // 滚动到距离底部200px时提前触发加载 if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !_scrollController.position.outOfRange) { if (!_isLoading && _hasMore) { pageNumber++; getRecipeList(); } } }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.rtl, child: Scaffold( appBar: AppBar( title: Text(widget.recipeCourseName), centerTitle: true, elevation: 5, ), body: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 12.0), child: _buildListView(), ), ), ); } Widget _buildListView() { // 首次加载中 if (_allRecipes.isEmpty && _isLoading) { return const Center(child: CircularProgressIndicator()); } // 加载失败 if (_allRecipes.isEmpty && !_isLoading) { return const Center(child: Text('加载失败,请下拉重试')); } return GridView.builder( controller: _scrollController, // 多出来的1项是底部的加载/无更多提示 itemCount: _allRecipes.length + 1, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12.0, mainAxisSpacing: 12.0, ), itemBuilder: (BuildContext context, int index) { // 渲染底部提示 if (index == _allRecipes.length) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center( child: _hasMore ? const CircularProgressIndicator() : const Text('暂无更多内容'), ), ); } final myRecipe = _allRecipes[index].recipe; return InkWell( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => RecipeDetails( recipe: _allRecipes[index], ), ), ); }, child: RecipeCard(myRecipe: myRecipe), ); }, ); } }
核心修改说明
- 新增三个状态变量管控加载逻辑:
_allRecipes存储全量列表数据,_isLoading防止重复请求,_hasMore标记是否还有更多数据 - 修正接口请求的分页参数,添加
page=前缀,后端可以正常识别分页页码 - 移除FutureBuilder,改用自定义的列表渲染逻辑,避免页面重绘重复触发接口请求
- 滚动监听增加阈值触发,提前加载下一页提升使用体验,同时加状态拦截避免重复请求
- GridView底部追加加载状态和无更多数据提示,符合需求要求
内容的提问来源于stack exchange,提问作者Adel5151
相关产品推荐
相关产品推荐

