Flutter鸿蒙开发指南(十三):推荐列表上拉加载
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net值此新春佳节,祝大家新年快乐!愿各位在新的一年里技术精进,薪资更上一层楼!
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
值此新春佳节,祝大家新年快乐!愿各位在新的一年里技术精进,薪资更上一层楼!
上拉加载实现要素:
- 使用原有接口实现
- 监听滚动到底部事件
- 同时只能加载一个请求
- 如果没有下一页不能再发起请求
一、功能描述
在移动应用开发中,上拉加载更多是一个核心的交互功能。本文将详细解析上拉加载功能的完整实现方案。该功能允许用户在滚动到列表底部时自动加载下一页数据,提供流畅的无限滚动体验。
二、核心实现要素分析
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());
}
}
四、运行效果
运行到鸿蒙模拟器,效果如下:

更多推荐


所有评论(0)