前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

值此新春佳节,祝大家新年快乐!愿各位在新的一年里技术精进,薪资更上一层楼!

上拉加载实现要素:

  1. 使用原有接口实现
  2. 监听滚动到底部事件
  3. 同时只能加载一个请求
  4. 如果没有下一页不能再发起请求

一、功能描述

在移动应用开发中,上拉加载更多是一个核心的交互功能。本文将详细解析上拉加载功能的完整实现方案。该功能允许用户在滚动到列表底部时自动加载下一页数据,提供流畅的无限滚动体验。

二、核心实现要素分析

2.1 态变量的声明与作用

变量作用说明:

  • _page:记录当前已加载的页码,每次成功加载后自增
  • _isLoading:请求锁,确保同一时间只有一个请求在进行
  • _hasMore:数据结束标志,当返回数据不足时设为false
// 推荐列表 - 存储要展示的数据
List<GoodDetailItem> _recommendList = [];

// 页码 - 记录当前加载到第几页
int _page = 1;

// 当前正在加载状态 - 防止重复请求
bool _isLoading = false;

// 是否还有下一页 - 控制是否继续加载
bool _hasMore = true;

    2.2 数据加载方法的完整实现

    该方法实现了带完整控制逻辑的数据加载。首先检查是否满足加载条件,满足则加锁防止重复请求,然后计算请求参数并发起网络调用。获取数据后解锁并更新UI,最后根据返回数据量判断是否还有下一页,如有则页码自增。

      // 获取推荐列表
      void _getRecommendList() async {
        //当已经有请求正在加载 或者 已经没有下一页了 就放弃请求
        if (_isLoading || !_hasMore) {
          return;
        }
        _isLoading = true; //占住位置
        int requestLimit = _page * 8;
        _recommendList = await getRecommendListAPI({"limit": requestLimit});
        _isLoading = false; //放开位置
        setState(() {});
        //我要10条 你给10条 说明我要的你都给了
        //我要10条 你给9条 意味着没满足我当前要求 意味着没有下一页了
        if(_recommendList.length < requestLimit){
          _hasMore = false;
          return;
        }
        _page++; //请求完成变成第一页,下次变成第二页
      }

    2.3 滚动监听机制

    本方法通过ScrollController监听滚动事件,实时获取当前滚动位置和最大滚动距离。设置50像素的提前触发阈值,当用户滚动接近底部时自动调用数据加载方法,提供流畅的加载体验。

    // 声明滚动控制器
    final ScrollController _controller = ScrollController();
    
    // 监听滚动到底部的事件
    void _registerEvent() {
      _controller.addListener(() {
        // 当前滚动位置
        double currentPosition = _controller.position.pixels;
        // 最大滚动距离
        double maxPosition = _controller.position.maxScrollExtent;
        // 触发阈值(距离底部50像素)
        double threshold = maxPosition - 50;
        
        // 调试信息
        debugPrint('📜 滚动位置:$currentPosition / $maxPosition');
        
        // 当滚动位置达到或超过阈值时触发加载
        if (currentPosition >= threshold) {
          debugPrint('🎯 触发上拉加载更多');
          _getRecommendList();
        }
      });
    }

    2.4 完整初始化流程

    在initState中完成所有初始化工作,包括加载首页各模块数据和注册滚动监听。各数据加载方法并行执行提高效率,滚动监听最后注册避免初始化过程中误触发。

    @override
    void initState() {
      super.initState();
      
      // 1. 加载轮播图数据
      _getBannederList();
      
      // 2. 加载分类数据
      _getCategoryList();
      
      // 3. 加载特惠推荐数据
      _getProductList();
      
      // 4. 加载热榜推荐数据
      _getInVogueList();
      
      // 5. 加载一站式推荐数据
      _getOneStopList();
      
      // 6. 加载推荐列表(第一页)
      _getRecommendList();
      
      // 7. 注册滚动监听事件
      _registerEvent();
    }

    三、核心实现逻辑

    3.1 分页数据判断机制

    分页判断的核心逻辑是基于数据量的对比。每次请求时,计算应该获取的数据总量,然后与接口实际返回的数据量进行比较。如果实际返回的数据量小于请求的数据量,说明服务器已经没有更多数据可提供,此时将_hasMore设为false,停止后续加载。如果数据量相等,说明还有更多数据,页码自增继续加载。

    // 请求数据量计算
    int requestLimit = _page * 8;
    
    // 数据量判断逻辑
    // 假设:
    // 第1页:请求8条,如果返回8条 => 还有下一页,_page=2
    // 第1页:请求8条,如果返回5条 => 没有下一页,_hasMore=false
    // 第2页:请求16条,如果返回16条 => 还有下一页,_page=3
    // 第2页:请求16条,如果返回12条 => 没有下一页,_hasMore=false
    
    if(_recommendList.length < requestLimit){
      _hasMore = false;  // 返回数据不足,标记为没有更多
      return;
    }
    _page++; // 数据充足,页码自增

    3.2 请求防重复机制

    通过_isLoading变量实现请求锁机制。每次发起请求前先检查_isLoading状态,如果为true说明已有请求在执行,直接返回不处理。只有在_isLoading为false时才能发起新请求。请求开始时将_isLoading设为true,请求结束后无论成功失败都设为false,确保请求的串行执行。

    // 使用_isLoading作为请求锁
    if (_isLoading || !_hasMore) {
      return; // 条件不满足时直接返回,不发起请求
    }
    
    _isLoading = true; // 上锁
    // 执行网络请求...
    _isLoading = false; // 解锁

    3.3 滚动阈值触发机制

    设置50像素的提前触发阈值,当滚动位置距离底部还有50像素时就开始加载下一页数据。这样设计的目的是为了提供更好的用户体验:给网络请求预留缓冲时间,避免用户滚动到底部时出现明显的加载等待;同时防止因滚动到底部才触发造成的卡顿感,使滚动过程更加流畅自然。

    // 使用50像素作为提前触发阈值
    if (_controller.position.pixels >=
        _controller.position.maxScrollExtent - 50) {
      _getRecommendList(); // 触发加载
    }
    
    // 阈值的作用:
    // - 提前50像素触发,保证加载过程的流畅性
    // - 避免到达底部时才触发造成的卡顿感
    // - 给网络请求预留缓冲时间

    3.4 完整代码

    lib/pages/Home/index.dart代码:

    import 'package:flutter/cupertino.dart';
    import 'package:qing_mall/api/home.dart';
    import 'package:qing_mall/components/Home/HmCategory.dart';
    import 'package:qing_mall/components/Home/HmHot.dart';
    import 'package:qing_mall/components/Home/HmMoreList.dart';
    import 'package:qing_mall/components/Home/HmSlider.dart';
    import 'package:qing_mall/components/Home/HmSuggestion.dart';
    import 'package:qing_mall/viewmodels/home.dart';
    
    class HomeView extends StatefulWidget {
      const HomeView({super.key});
    
      @override
      State<HomeView> createState() => _HomeViewState();
    }
    
    class _HomeViewState extends State<HomeView> {
      //分类列表
      List<CategoryItem> _categoryList = [];
    
      //轮播图列表
      List<BannerItem> _bannerList = [
        // BannerItem(
        //   id: "1",
        //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/1.jpg",
        // ),
        // BannerItem(
        //   id: "2",
        //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/2.png",
        // ),
        // BannerItem(
        //   id: "3",
        //   imgUrl: "https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/meituan/3.jpg",
        // ),
      ];
    
      //获取滚动容器的内容
    
      List<Widget> _getScrollChildren() {
        return [
          //包裹普通widget的sliver家族的组件内容
          SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)), //轮播图组件
          //放置分类组件
          SliverToBoxAdapter(child: SizedBox(height: 10)),
          //SliverGrid SliverList指南纵向排列
          SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)), //分类组件
          SliverToBoxAdapter(child: SizedBox(height: 10)),
          SliverToBoxAdapter(
              child: HmSuggestion(specialOfferResult: _specialOfferResult)), //推荐组件
          SliverToBoxAdapter(child: SizedBox(height: 10)),
    
          //Flex和Expanded配合起来可以均分比例
          SliverToBoxAdapter(
            child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 10),
                child: Flex(
                  direction: Axis.horizontal,
                  children: [
                    Expanded(
                      child: HmHot(result: _inVogueResult, type: "hot"),
                    ),
                    SizedBox(width: 10),
                    Expanded(
                      child: HmHot(result: _oneStopResult, type: "step"),
                    ),
                  ],
                )),
          ),
          SliverToBoxAdapter(child: SizedBox(height: 10)),
    
          HmMoreList(recommendList: _recommendList), // 无限滚动列表
        ];
      }
    
      //特惠推荐
      SpecialOfferResult _specialOfferResult = SpecialOfferResult(
        id: "",
        title: "",
        subTypes: [],
      );
    
      // 热榜推荐
      SpecialOfferResult _inVogueResult = SpecialOfferResult(
        id: "",
        title: "",
        subTypes: [],
      );
    
      // 一站式推荐
      SpecialOfferResult _oneStopResult = SpecialOfferResult(
        id: "",
        title: "",
        subTypes: [],
      );
    
      // 推荐列表
      List<GoodDetailItem> _recommendList = [];
    
      // 页码
      int _page = 1;
    
      // 当前正在加载状态
      bool _isLoading = false;
    
      //是否还有下一页
      bool _hasMore = true;
    
      // 获取推荐列表
      void _getRecommendList() async {
        //当已经有请求正在加载 或者 已经没有下一页了 就放弃请求
        if (_isLoading || !_hasMore) {
          return;
        }
        _isLoading = true; //占住位置
        int requestLimit = _page * 8;
        _recommendList = await getRecommendListAPI({"limit": requestLimit});
        _isLoading = false; //放开位置
        setState(() {});
        //我要10条 你给10条 说明我要的你都给了
        //我要10条 你给9条 意味着没满足我当前要求 意味着没有下一页了
        if(_recommendList.length < requestLimit){
          _hasMore = false;
          return;
        }
        _page++; //请求完成变成第一页,下次变成第二页
      }
    
    // 获取热榜推荐列表
      void _getInVogueList() async {
        _inVogueResult = await getInVogueListAPI();
        setState(() {});
      }
    
      // 获取一站式推荐列表
      void _getOneStopList() async {
        _oneStopResult = await getOneStopListAPI();
        setState(() {});
      }
    
      @override
      void initState() {
        super.initState();
        _getBannederList();
        _getCategoryList();
        _getProductList();
        _getInVogueList();
        _getOneStopList();
        _getRecommendList();
        _registerEvent();
      }
    
      //监听滚动到底部的事件
      void _registerEvent() {
        _controller.addListener(() {
          //如果滚动的距离 = 滚动到底部的最大距离 - 50
          if (_controller.position.pixels >=
              _controller.position.maxScrollExtent - 50) {
            //加载下一页数据
            _getRecommendList();
          }
        });
      }
    
      //获取特惠推荐
      void _getProductList() async {
        _specialOfferResult = await getProductListAPI();
        setState(() {});
      }
    
      //获取分类列表
      void _getCategoryList() async {
        _categoryList = await getCategoryListAPI();
        setState(() {});
      }
    
      void _getBannederList() async {
        _bannerList = await getBannerListAPI();
        setState(() {});
      }
    
      final ScrollController _controller = ScrollController();
    
      @override
      Widget build(BuildContext context) {
        //CustomScrollview要求:必须是sliver家族的内容
        return CustomScrollView(
            controller: _controller, //绑定控制器
            slivers: _getScrollChildren());
      }
    }
    

    四、运行效果

    运行到鸿蒙模拟器,效果如下:

    Logo

    开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

    更多推荐