Flutter鸿蒙开发指南(十三):推荐列表上拉加载

前言

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

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

上拉加载实现要素:

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

一、功能描述

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

二、核心实现要素分析

2.1 态变量的声明与作用

变量作用说明:

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

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

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

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

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

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

Dart 复制代码
  // 获取推荐列表
  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像素的提前触发阈值,当用户滚动接近底部时自动调用数据加载方法,提供流畅的加载体验。

Dart 复制代码
// 声明滚动控制器
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中完成所有初始化工作,包括加载首页各模块数据和注册滚动监听。各数据加载方法并行执行提高效率,滚动监听最后注册避免初始化过程中误触发。

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

三、核心实现逻辑

3.1 分页数据判断机制

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

Dart 复制代码
// 请求数据量计算
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,确保请求的串行执行。

Dart 复制代码
// 使用_isLoading作为请求锁
if (_isLoading || !_hasMore) {
  return; // 条件不满足时直接返回,不发起请求
}

_isLoading = true; // 上锁
// 执行网络请求...
_isLoading = false; // 解锁

3.3 滚动阈值触发机制

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

Dart 复制代码
// 使用50像素作为提前触发阈值
if (_controller.position.pixels >=
    _controller.position.maxScrollExtent - 50) {
  _getRecommendList(); // 触发加载
}

// 阈值的作用:
// - 提前50像素触发,保证加载过程的流畅性
// - 避免到达底部时才触发造成的卡顿感
// - 给网络请求预留缓冲时间

3.4 完整代码

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

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());
  }
}

四、运行效果

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

相关推荐
键盘鼓手苏苏3 小时前
Flutter for OpenHarmony:debounce_throttle 防抖与节流的艺术(优化用户交互与网络请求) 深度解析与鸿蒙适配指南
网络·flutter·交互
阿林来了3 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别监听器实现
人工智能·flutter·语音识别·harmonyos
松叶似针3 小时前
Flutter三方库适配OpenHarmony【secure_application】— setWindowPrivacyMode 隐私模式实现
flutter·harmonyos
哈__4 小时前
基础入门 Flutter for OpenHarmony:image_cropper 图片裁剪详解
flutter
哈__4 小时前
基础入门 Flutter for OpenHarmony:flutter_contacts 通讯录管理详解
flutter
键盘鼓手苏苏5 小时前
Flutter for OpenHarmony:cider 自动化版本管理与变更日志生成器(发布流程标准化的瑞士军刀) 深度解析与鸿蒙适配指南
运维·开发语言·flutter·华为·rust·自动化·harmonyos
松叶似针5 小时前
Flutter三方库适配OpenHarmony【secure_application】— onMethodCall 方法分发实现
flutter·harmonyos
阿林来了5 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— 语音识别引擎创建
人工智能·flutter·语音识别·harmonyos
键盘鼓手苏苏5 小时前
Flutter for OpenHarmony:dart_ping 网络诊断的瑞士军刀(支持 ICMP Ping) 深度解析与鸿蒙适配指南
开发语言·网络·flutter·华为·rust·harmonyos