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

Flutter REST API分页异常:数据加载完成后重复加载无提示

Flutter分页加载重复数据且不显示“已加载全部”的问题修复

问题描述

Flutter应用中通过HTTP调用REST API实现分页功能,基础分页逻辑可运行,但所有数据加载完成后,未显示“已加载全部数据”提示,反而重复加载顶部旧数据,持续生成重复内容。

错误原因分析

  1. API请求参数格式错误:原请求URL未指定分页参数名(如page、limit),后端无法正确识别分页参数,导致每次请求都返回第一页数据。
  2. 无效的重复数据校验:if (!fetchedPosts.contains(fetchedPosts))逻辑错误,列表必然包含自身,导致所有重复数据都会被添加到列表中。
  3. 分页终止逻辑未触发:由于每次请求都返回非空的第一页数据,_hasNextPage永远不会被设为false,“已加载全部”提示无法显示。

修复步骤

1. 修正API请求URL

为分页参数添加明确的参数名,确保后端能正确解析:

// 原代码
final res = await http.get(Uri.parse("$_baseUrl?$_page&$_limit"));

// 修改后
final res = await http.get(Uri.parse("$_baseUrl?page=$_page&limit=$_limit"));

2. 删除无效的重复数据判断

移除逻辑错误的重复校验代码,直接添加新获取的数据:

// 原代码
if (fetchedPosts.isNotEmpty) {
  if (!fetchedPosts.contains(fetchedPosts)) {
    setState(() {
      _posts.addAll(fetchedPosts);
    });
  }
}

// 修改后
if (fetchedPosts.isNotEmpty) {
  setState(() {
    _posts.addAll(fetchedPosts);
  });
}

3. 确保分页终止逻辑正常触发

当后端返回空列表时,正确设置_hasNextPage = false,停止后续加载请求:

else {
  setState(() {
    _hasNextPage = false;
  });
}

完整修复后的代码

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_browser/flutter_web_browser.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'dart:convert';
import "package:http/http.dart" as http;
import 'package:voterlist/Widgets/adhelper.dart';
import 'package:voterlist/Widgets/constant.dart';
import 'package:voterlist/Widgets/skeleton.dart';
import 'package:voterlist/Widgets/widget.dart';

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

  @override
  State<Jobs> createState() => _JobsState();
}

class _JobsState extends State<Jobs> {
  final adManager = AdManager();

  // 数据接口地址
  final _baseUrl = 'api';

  // 初始页码
  int _page = 0;
  // 每页加载数量
  final int _limit = 15;

  // 是否还有下一页数据
  bool _hasNextPage = true;

  // 首次加载状态标记
  bool _isFirstLoadRunning = false;

  // 加载更多状态标记
  bool _isLoadMoreRunning = false;

  // 存储获取到的数据
  List _posts = [];

  // 首次加载数据
  void _firstLoad() async {
    setState(() {
      _isFirstLoadRunning = true;
    });
    try {
      // 修复:添加分页参数名
      final res = await http.get(Uri.parse("$_baseUrl?page=$_page&limit=$_limit"));
      setState(() {
        _posts = json.decode(res.body);
      });
    } catch (err) {
      if (kDebugMode) {
        print('请求失败');
      }
    }

    setState(() {
      _isFirstLoadRunning = false;
    });
  }

  // 滚动到底部时加载更多数据
  void _loadMore() async {
    if (_hasNextPage == true &&
        _isFirstLoadRunning == false &&
        _isLoadMoreRunning == false &&
        _controller.position.extentAfter < 300) {
      setState(() {
        _isLoadMoreRunning = true; // 显示底部加载指示器
      });
      _page += 1; // 页码+1
      try {
        // 修复:添加分页参数名
        final res = await http.get(Uri.parse("$_baseUrl?page=$_page&limit=$_limit"));

        final List fetchedPosts = json.decode(res.body);
        if (fetchedPosts.isNotEmpty) {
          // 修复:删除无效的重复判断
          setState(() {
            _posts.addAll(fetchedPosts);
          });
        } else {
          // 没有更多数据,终止分页
          setState(() {
            _hasNextPage = false;
          });
        }
      } catch (err) {
        if (kDebugMode) {
          print('加载更多失败');
        }
      }

      setState(() {
        _isLoadMoreRunning = false;
      });
    }
  }

  // 列表滚动控制器
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();
    _firstLoad();
    _controller = ScrollController()..addListener(_loadMore);
    adManager.addAds(true, true, true);
    adManager.showInterstitial();
  }

  @override
  void dispose() {
    _controller.removeListener(_loadMore);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: headerNav(title: "Sarkari Naukri"),
      body: _isFirstLoadRunning
          ? ListView.separated(
              itemCount: 5,
              itemBuilder: (context, index) => const NewsCardSkelton(),
              separatorBuilder: (context, index) =>
                  const SizedBox(height: defaultPadding),
            )
          : Column(
              children: [
                Expanded(
                  child: ListView.separated(
                    controller: _controller,
                    itemCount: _posts.length,
                    itemBuilder: (_, index) => JobCard(
                      onPress: () {
                        FlutterWebBrowser.openWebPage(
                          url: _posts[index]['moredetils'],
                          safariVCOptions: const SafariViewControllerOptions(
                            barCollapsingEnabled: true,
                            preferredBarTintColor: Colors.green,
                            preferredControlTintColor: Colors.amber,
                            dismissButtonStyle:
                                SafariViewControllerDismissButtonStyle.close,
                            modalPresentationCapturesStatusBarAppearance: true,
                          ),
                        );
                      },
                      image: _posts[index]['imagelink'],
                      state: _posts[index]['state'],
                      title: _posts[index]['title'],
                      subtitle: _posts[index]['totalpost'].toString(),
                    ),
                    separatorBuilder: (BuildContext context, int index) {
                      if ((index + 1) % 4 == 0) {
                        return Container(
                          width: adManager.getBannerAd()?.size.width.toDouble(),
                          height:
                              adManager.getBannerAd()?.size.height.toDouble(),
                          child: AdWidget(ad: adManager.getBannerAd()!),
                        );
                      }
                      return const SizedBox();
                    },
                  ),
                ),

                // 加载更多时显示进度指示器
                if (_isLoadMoreRunning == true)
                  const Padding(
                    padding: EdgeInsets.only(top: 10, bottom: 40),
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  ),

                // 没有更多数据时显示提示
                if (_hasNextPage == false)
                  Container(
                    padding: const EdgeInsets.only(top: 30, bottom: 40),
                    color: Colors.amber,
                    child: const Center(
                      child: Text('已加载全部职位'),
                    ),
                  ),
              ],
            ),
    );
  }
}

class NewsCardSkelton extends StatelessWidget {
  const NewsCardSkelton({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Skeleton(height: 120, width: 120),
        const SizedBox(width: defaultPadding),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Skeleton(width: 80),
              const SizedBox(height: defaultPadding / 2),
              const Skeleton(),
              const SizedBox(height: defaultPadding / 2),
              const Skeleton(),
              const SizedBox(height: defaultPadding / 2),
              Row(
                children: const [
                  Expanded(
                    child: Skeleton(),
                  ),
                  SizedBox(width: defaultPadding),
                  Expanded(
                    child: Skeleton(),
                  ),
                ],
              )
            ],
          ),
        )
      ],
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 19:46:16