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

如何为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),
        );
      },
    );
  }
}

核心修改说明

  1. 新增三个状态变量管控加载逻辑:_allRecipes存储全量列表数据,_isLoading防止重复请求,_hasMore标记是否还有更多数据
  2. 修正接口请求的分页参数,添加page=前缀,后端可以正常识别分页页码
  3. 移除FutureBuilder,改用自定义的列表渲染逻辑,避免页面重绘重复触发接口请求
  4. 滚动监听增加阈值触发,提前加载下一页提升使用体验,同时加状态拦截避免重复请求
  5. GridView底部追加加载状态和无更多数据提示,符合需求要求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.10 07:06:00