flutter三方库适配鸿蒙(flutter_refresh)实战+上拉加载
文章摘要 本文详细介绍了Flutter中实现上拉加载功能的原理和代码实现。上拉加载与下拉刷新的核心区别在于:下拉刷新是重置数据,而上拉加载是追加数据。文章剖析了_RefreshFooterHandler类的实现原理,包括触发加载的阈值计算和加载完成后的滚动位置调整。在代码实现部分,重点讲解了状态管理(当前页码、是否有更多数据)、列表项构建、以及上拉加载回调中的分页逻辑和错误处理。最后还提到下拉刷新
前言
上一篇讲了下拉刷新的实现,这篇继续聊上拉加载。其实原理差不多,就是换了个回调,但实际开发中上拉加载有一些细节需要注意,比如分页逻辑、加载完成的判断等。
这篇文章会结合实际场景,把这些细节都过一遍。
—
一、上拉加载和下拉刷新的区别
虽然都是刷新操作,但两者的业务逻辑完全不同:
下拉刷新:重新获取第一页数据,替换掉现有列表。用户的预期是"看看有没有新内容"。
上拉加载:获取下一页数据,追加到现有列表末尾。用户的预期是"看更多内容"。
这个区别决定了我们在回调里要做的事情不一样。下拉刷新是"重置",上拉加载是"追加"。
二、库里上拉加载的实现原理
在开始写代码之前,先看看库是怎么处理上拉加载的。打开 lib/src/refresh.dart,找到 _RefreshFooterHandler 这个类:
class _RefreshFooterHandler extends _RefreshHandler {
_RefreshFooterHandler(
{required RefreshWidgetController controller,
required RefresherCallback callback,
double offset = 50.0})
: super(controller: controller, callback: callback, offset: offset);
这是上拉加载的处理器,继承自 _RefreshHandler。offset 参数是触发加载的阈值,默认 50 像素。也就是说,用户往上拉超过 50 像素就会触发加载。
再看看它怎么判断滚动位置的:
double getRefreshWidgetMoveValue(ScrollMetrics metrics) {
return metrics.pixels - metrics.maxScrollExtent;
}
metrics.pixels 是当前滚动位置,metrics.maxScrollExtent 是最大可滚动距离。当用户滚动到底部并继续往上拉时,pixels 会超过 maxScrollExtent,差值就是"过度滚动"的距离。
这个差值超过 offset(默认 50)就会触发加载回调。
还有一个细节,看看加载完成后的处理:
if (refreshHandle == _footerHandler && maxExtent != null) {
controller?.jumpTo(maxExtent - _footerRefreshOffset * 2);
}
加载完成后,库会把滚动位置往回调一点。为什么?因为加载完新数据后,列表变长了,maxScrollExtent 也变大了。如果不调整位置,用户会感觉列表"跳"了一下。这个处理让体验更平滑。
三、开始写代码
3.1 定义状态变量
上拉加载需要维护更多的状态:
class _RefreshDemoState extends State<RefreshDemo> {
List<String> _dataList = [];
int _currentPage = 1;
bool _hasMore = true;
_dataList 存放列表数据,用 List 而不是简单的 int,因为实际项目中数据肯定是个列表。
_currentPage 记录当前页码,每次加载成功后加 1。
_hasMore 标记是否还有更多数据。当服务端返回的数据少于一页的数量时,说明没有更多了,这时候就不应该再触发加载。
3.2 初始化数据
void initState() {
super.initState();
_loadInitialData();
}
void _loadInitialData() {
// 模拟初始数据
_dataList = List.generate(10, (index) => 'Item ${index + 1}');
}
initState 是 State 的生命周期方法,在组件创建时调用一次。这里用来加载初始数据。
List.generate 是 Dart 的便捷方法,生成一个指定长度的列表。第一个参数是长度,第二个参数是生成每个元素的函数。
3.3 构建列表项
Widget _itemBuilder(BuildContext context, int index) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: Colors.grey.shade200),
),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: Colors.blue.shade100,
child: Text(
'${index + 1}',
style: TextStyle(color: Colors.blue.shade700),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_dataList[index],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
'上拉加载更多数据',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
这次没用 ListTile,而是自己写了布局。实际项目中列表项的样式往往比较复杂,ListTile 不一定能满足需求。
Container 加 decoration 实现底部分割线。用 Border 而不是 Divider 组件,是因为 Divider 会占用额外的空间。
Row 横向排列头像和文字。Expanded 让文字部分占满剩余空间,避免文字太长时溢出。
Column 纵向排列标题和副标题。crossAxisAlignment: CrossAxisAlignment.start 让文字左对齐。
3.4 实现上拉加载回调
这是核心逻辑:
Future<void> onFooterRefresh() async {
if (!_hasMore) {
return;
}
await Future.delayed(const Duration(seconds: 1));
final newPage = _currentPage + 1;
final newData = List.generate(
10,
(index) => 'Item ${(_currentPage * 10) + index + 1}',
);
setState(() {
_dataList.addAll(newData);
_currentPage = newPage;
// 模拟加载到第5页后没有更多数据
if (_currentPage >= 5) {
_hasMore = false;
}
});
}
先看第一个判断:
if (!_hasMore) {
return;
}
如果已经没有更多数据了,直接返回,不执行加载逻辑。这个判断很重要,不然用户会一直看到加载动画,体验很差。
Future.delayed 模拟网络请求的延迟。实际项目中这里是调接口:
Future<void> onFooterRefresh() async {
if (!_hasMore) return;
try {
final response = await api.fetchList(page: _currentPage + 1);
setState(() {
_dataList.addAll(response.data);
_currentPage++;
_hasMore = response.data.length >= pageSize;
});
} catch (e) {
// 加载失败的处理
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载失败: $e')),
);
}
}
_hasMore 的判断逻辑是:如果返回的数据量小于一页的数量(pageSize),说明服务端已经没有更多数据了。
addAll 是 List 的方法,把新数据追加到列表末尾。注意是"追加"不是"替换",这是上拉加载和下拉刷新的关键区别。
3.5 实现下拉刷新回调
上拉加载通常要配合下拉刷新一起用。下拉刷新的逻辑是重置所有状态:
Future<void> onHeaderRefresh() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
_dataList = List.generate(10, (index) => 'Item ${index + 1}');
_currentPage = 1;
_hasMore = true;
});
}
三个状态都要重置:
_dataList重新生成,相当于获取第一页数据_currentPage重置为 1_hasMore重置为 true,允许继续加载
如果只重置数据不重置 _hasMore,用户刷新后就没法继续上拉加载了,这是个常见的 bug。
3.6 组装 Refresh 组件
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('上拉加载示例'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SafeArea(
child: Refresh(
onHeaderRefresh: onHeaderRefresh,
onFooterRefresh: onFooterRefresh,
childBuilder: (BuildContext context,
{ScrollController? controller, ScrollPhysics? physics}) {
return ListView.builder(
controller: controller,
physics: physics,
itemBuilder: _itemBuilder,
itemCount: _dataList.length,
);
},
),
),
);
}
和下拉刷新的区别就是多了 onFooterRefresh 参数。两个回调可以同时配置,也可以只配置其中一个。
itemCount 用的是 _dataList.length 而不是固定值,因为上拉加载会动态增加数据。
四、一些进阶处理
4.1 显示"没有更多了"提示
当 _hasMore 为 false 时,可以在列表底部显示一个提示:
Widget _itemBuilder(BuildContext context, int index) {
// 最后一项显示"没有更多了"
if (index == _dataList.length) {
return Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: Text(
'没有更多了',
style: TextStyle(color: Colors.grey.shade500),
),
);
}
// 正常的列表项
return Container(
// ... 省略
);
}
同时要修改 itemCount:
itemCount: _hasMore ? _dataList.length : _dataList.length + 1,
当还有更多数据时,itemCount 就是数据长度;当没有更多数据时,多加一项用来显示提示。
4.2 加载失败的重试
网络请求可能失败,失败后应该允许用户重试:
bool _loadError = false;
Future<void> onFooterRefresh() async {
if (!_hasMore) return;
try {
// ... 加载逻辑
_loadError = false;
} catch (e) {
setState(() {
_loadError = true;
});
}
}
然后在列表底部显示重试按钮:
if (_loadError) {
return GestureDetector(
onTap: onFooterRefresh,
child: Container(
padding: const EdgeInsets.all(16),
alignment: Alignment.center,
child: const Text(
'加载失败,点击重试',
style: TextStyle(color: Colors.red),
),
),
);
}
GestureDetector 是手势检测组件,onTap 处理点击事件。点击后重新调用加载方法。
4.3 空列表的处理
如果列表为空,应该显示一个空状态页面:
Widget build(BuildContext context) {
return Scaffold(
// ... appBar
body: SafeArea(
child: _dataList.isEmpty
? _buildEmptyView()
: Refresh(
// ... 正常的列表
),
),
);
}
Widget _buildEmptyView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'暂无数据',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
),
),
],
),
);
}
空状态页面不需要 Refresh 组件包裹,因为没有内容可以滚动。
五、运行到鸿蒙设备
5.1 检查设备
flutter devices
确认鸿蒙设备已连接并授权。
5.2 运行
flutter run -d <设备ID>
第一次编译会比较慢,后续热重载就很快了。
5.3 测试上拉加载
应用启动后,滑动到列表底部,继续往上拉,应该能看到加载动画。松手后会加载新数据,列表会变长。
多拉几次,加载到第 5 页后就不会再触发加载了(因为我们设置了 _hasMore = false)。
下拉刷新后,状态会重置,又可以继续上拉加载了。
六、常见问题
6.1 上拉加载不触发
检查几个地方:
controller和physics有没有传给子列表_hasMore是不是 false 了- 列表内容是不是不够一屏(不够一屏的话滚不到底部)
6.2 数据重复
检查 addAll 的逻辑,确保不是每次都从第一页开始加载。_currentPage 要正确递增。
6.3 加载动画不消失
检查回调是不是返回了 Future。如果回调里有 async,确保所有异步操作都 await 了。
七、小结
上拉加载的核心就是 onFooterRefresh 回调,配合分页状态管理。实际项目中还要处理加载失败、空列表、没有更多数据等边界情况。
flutter_refresh 这个库把滚动监听、动画效果都封装好了,我们只需要关注业务逻辑就行。而且因为是纯 Dart 实现,在鸿蒙上跑起来完全没问题。
两篇文章把下拉刷新和上拉加载都讲完了,基本覆盖了列表页面的常见需求。如果有更复杂的场景,可以看看库的源码,自己扩展一下。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)