在 Flutter 开发中,下拉刷新、上拉加载更多是商品列表、消息流、资讯页等场景的核心交互。原生RefreshIndicator仅支持基础下拉刷新,上拉加载需手动处理滚动监听、加载状态控制、重复请求拦截等复杂逻辑,导致代码冗余、体验参差不齐。本文封装的PullRefreshList组件,整合 "下拉刷新 + 上拉加载 + 多状态提示 + 防重复请求" 四大核心能力,一行代码即可搭建功能完整、体验流畅的列表,适配 90%+ 高频场景!
一、核心需求拆解
✅ 下拉刷新:支持原生风格刷新指示器,可自定义刷新样式✅ 上拉加载:滚动到底部前 200px 自动触发,避免用户等待✅ 多状态提示:内置加载中、无更多数据、加载失败三种状态,支持自定义文本样式✅ 防重复加载:通过状态锁拦截重复请求,提升稳定性✅ 灵活配置:支持禁用下拉 / 上拉功能,自定义列表内边距、初始数据✅ 高度通用:通过泛型支持任意数据类型,列表项完全自定义
二、完整代码实现(可直接复制使用)
dart
import 'package:flutter/material.dart';
// 列表加载状态枚举(精准控制加载逻辑)
enum LoadStatus {
idle, // 空闲状态(可触发加载)
loading, // 加载中(拦截重复请求)
noMore, // 无更多数据(停止上拉加载)
error // 加载失败(支持点击重试)
}
/// 通用下拉刷新上拉加载列表组件
class PullRefreshList<T> extends StatefulWidget {
// 必选参数:核心回调与构建器
final Future<List<T>> Function() onRefresh; // 下拉刷新回调(返回新数据)
final Future<List<T>> Function() onLoadMore; // 上拉加载回调(返回更多数据)
final Widget Function(T item) itemBuilder; // 列表项构建器(自定义列表样式)
// 可选参数:初始化与配置
final List<T> initialData; // 初始数据(页面加载时默认显示)
final Widget? refreshIndicator; // 自定义下拉刷新指示器(优先级高于原生)
final String loadingText; // 加载中提示文本(默认"加载中...")
final String noMoreText; // 无更多数据提示文本(默认"没有更多数据了")
final String errorText; // 加载失败提示文本(默认"加载失败,点击重试")
final TextStyle? statusTextStyle; // 状态提示文本样式
final EdgeInsetsGeometry padding; // 列表内边距(默认无内边距)
final bool enablePullRefresh; // 是否启用下拉刷新(默认true)
final bool enableLoadMore; // 是否启用上拉加载(默认true)
final ScrollPhysics? physics; // 滚动物理效果(默认适配平台)
const PullRefreshList({
super.key,
required this.onRefresh,
required this.onLoadMore,
required this.itemBuilder,
this.initialData = const [],
this.refreshIndicator,
this.loadingText = "加载中...",
this.noMoreText = "没有更多数据了",
this.errorText = "加载失败,点击重试",
this.statusTextStyle,
this.padding = const EdgeInsets.all(0),
this.enablePullRefresh = true,
this.enableLoadMore = true,
this.physics,
});
@override
State<PullRefreshList<T>> createState() => _PullRefreshListState<T>();
}
class _PullRefreshListState<T> extends State<PullRefreshList<T>> {
final ScrollController _scrollController = ScrollController(); // 滚动控制器
List<T> _dataList = []; // 列表数据源
LoadStatus _loadStatus = LoadStatus.idle; // 加载状态(默认空闲)
@override
void initState() {
super.initState();
_dataList = widget.initialData; // 初始化数据源
_scrollController.addListener(_onScroll); // 监听滚动事件
}
@override
void dispose() {
_scrollController.dispose(); // 释放控制器资源,避免内存泄漏
super.dispose();
}
/// 滚动监听:提前200px触发上拉加载,提升用户体验
void _onScroll() {
if (!widget.enableLoadMore) return; // 禁用上拉加载时直接返回
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMoreData(); // 触发加载更多
}
}
/// 下拉刷新逻辑:清空旧数据,加载新数据
Future<void> _refreshData() async {
if (_loadStatus == LoadStatus.loading) return; // 加载中拦截重复请求
try {
final newData = await widget.onRefresh(); // 调用外部刷新回调
setState(() {
_dataList = newData; // 替换数据源
_loadStatus = LoadStatus.idle; // 重置为空闲状态
});
} catch (e) {
setState(() => _loadStatus = LoadStatus.error); // 异常时设为错误状态
}
}
/// 上拉加载更多逻辑:追加新数据
Future<void> _loadMoreData() async {
if (_loadStatus != LoadStatus.idle) return; // 非空闲状态拦截请求
setState(() => _loadStatus = LoadStatus.loading); // 设为加载中状态
try {
final newData = await widget.onLoadMore(); // 调用外部加载回调
setState(() {
if (newData.isEmpty) {
_loadStatus = LoadStatus.noMore; // 无新数据时设为无更多状态
} else {
_dataList.addAll(newData); // 追加新数据
_loadStatus = LoadStatus.idle; // 重置为空闲状态
}
});
} catch (e) {
setState(() => _loadStatus = LoadStatus.error); // 异常时设为错误状态
}
}
/// 构建状态提示组件(加载中/无更多/加载失败)
Widget _buildStatusWidget() {
Widget statusWidget;
switch (_loadStatus) {
case LoadStatus.loading:
// 加载中:指示器+文本
statusWidget = Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(strokeWidth: 2), // 小尺寸指示器
const SizedBox(width: 8),
Text(widget.loadingText, style: widget.statusTextStyle),
],
),
),
);
break;
case LoadStatus.noMore:
// 无更多数据:纯文本
statusWidget = Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(widget.noMoreText, style: widget.statusTextStyle),
),
);
break;
case LoadStatus.error:
// 加载失败:可点击重试文本
statusWidget = Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: GestureDetector(
onTap: _loadMoreData, // 点击触发重试
child: Text(
widget.errorText,
style: widget.statusTextStyle?.copyWith(
color: Colors.blue,
decoration: TextDecoration.underline,
) ??
const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
fontSize: 14,
),
),
),
),
);
break;
default:
statusWidget = const SizedBox.shrink(); // 空闲状态不显示
}
return statusWidget;
}
@override
Widget build(BuildContext context) {
// 基础列表配置
final listView = ListView.builder(
controller: _scrollController,
padding: widget.padding,
physics: widget.physics ?? const AlwaysScrollableScrollPhysics(),
itemCount: _dataList.length + (widget.enableLoadMore ? 1 : 0), // 数据源长度+状态项
itemBuilder: (context, index) {
if (index < _dataList.length) {
return widget.itemBuilder(_dataList[index]); // 构建列表项
} else {
return _buildStatusWidget(); // 构建状态提示项
}
},
);
// 包装下拉刷新组件
return RefreshIndicator(
onRefresh: widget.enablePullRefresh ? _refreshData : () async {}, // 禁用时返回空回调
child: widget.refreshIndicator != null
? Stack(
children: [
listView,
Positioned(
top: 0,
left: 0,
right: 0,
child: widget.refreshIndicator!, // 自定义刷新指示器
),
],
)
: listView, // 原生刷新指示器
);
}
}
三、实战使用示例(覆盖 4 大高频场景)
场景 1:商品列表(基础用法)
适配电商商品列表,简洁配置 + 原生风格,快速实现核心功能:
dart
PullRefreshList<String>(
onRefresh: () async {
// 模拟下拉刷新请求(实际场景替换为接口请求)
await Future.delayed(const Duration(seconds: 1));
return ["商品1", "商品2", "商品3", "商品4", "商品5"];
},
onLoadMore: () async {
// 模拟上拉加载请求
await Future.delayed(const Duration(seconds: 1));
return ["商品6", "商品7", "商品8"]; // 返回空列表则触发"无更多数据"
},
itemBuilder: (item) {
// 自定义商品列表项
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: Color(0xFFF5F5F5))),
),
child: Row(
children: [
const Icon(Icons.shopping_cart, color: Colors.blue),
const SizedBox(width: 12),
Text(item, style: const TextStyle(fontSize: 16)),
],
),
);
},
padding: const EdgeInsets.symmetric(horizontal: 16),
statusTextStyle: const TextStyle(fontSize: 14, color: Colors.grey),
loadingText: "加载更多商品...",
noMoreText: "已加载全部商品",
);
场景 2:消息列表(带初始数据 + 自定义列表项)
适配聊天消息、系统通知列表,支持初始数据预加载:
dart
PullRefreshList<Map<String, String>>(
initialData: [
{"title": "系统通知", "content": "您的订单已发货,预计3天后送达"},
{"title": "好友消息", "content": "明天下午2点,线下门店见~"},
],
onRefresh: () async {
// 下拉刷新获取最新消息
await Future.delayed(const Duration(seconds: 1));
return [
{"title": "新通知", "content": "您的账户余额变动+100元"},
{"title": "系统通知", "content": "您的订单已发货,预计3天后送达"},
{"title": "好友消息", "content": "明天下午2点,线下门店见~"},
];
},
onLoadMore: () async {
// 上拉加载历史消息
await Future.delayed(const Duration(seconds: 1));
return [
{"title": "历史消息", "content": "上次的文件麻烦发我一下,谢谢~"},
{"title": "历史消息", "content": "周末一起去看电影吗?"},
];
},
itemBuilder: (item) {
// 自定义消息列表项(标题+内容)
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(bottom: BorderSide(color: Color(0xFFF5F5F5))),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item["title"]!,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
const SizedBox(height: 4),
Text(
item["content"]!,
style: const TextStyle(color: Color(0xFF666666), fontSize: 14),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
noMoreText: "已加载全部消息",
errorText: "消息加载失败,点击重新加载",
enableLoadMore: true,
);
场景 3:公告列表(仅下拉刷新,禁用上拉加载)
适配公告、新闻列表等 "无更多数据" 场景,仅保留下拉刷新功能:
dart
PullRefreshList<String>(
onRefresh: () async {
// 下拉刷新获取最新公告
await Future.delayed(const Duration(seconds: 1));
return [
"【重要通知】APP将于12月10日进行系统升级",
"【活动公告】双12购物节优惠活动规则",
"【版本更新】V3.13.0版本新增功能说明",
];
},
onLoadMore: () async => [], // 无需加载更多,返回空列表
itemBuilder: (item) {
return ListTile(
title: Text(item, style: const TextStyle(fontSize: 15)),
trailing: const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
);
},
enableLoadMore: false, // 禁用上拉加载
padding: const EdgeInsets.symmetric(horizontal: 16),
);
场景 4:自定义刷新指示器(品牌化风格)
适配需要个性化刷新样式的场景,替换原生刷新指示器:
dart
PullRefreshList<String>(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
return ["自定义刷新样式示例1", "自定义刷新样式示例2", "自定义刷新样式示例3"];
},
onLoadMore: () async {
await Future.delayed(const Duration(seconds: 1));
return ["自定义刷新样式示例4", "自定义刷新样式示例5"];
},
itemBuilder: (item) => ListTile(title: Text(item)),
refreshIndicator: const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Center(
child: Text(
"下拉刷新中...",
style: TextStyle(color: Colors.blue, fontSize: 14),
),
),
), // 自定义刷新提示
statusTextStyle: const TextStyle(fontSize: 14, color: Colors.blue),
);
四、核心封装技巧(让组件更稳定、更易用)
- 状态锁防重复请求 :通过
LoadStatus枚举控制状态,加载中时拦截下拉刷新和上拉加载的重复请求,避免接口并发调用导致的数据错乱。 - 提前触发加载更多:滚动到距离底部 200px 时触发加载,而非完全滚动到底部,减少用户等待感,提升交互体验。
- 泛型适配任意数据 :采用泛型
<T>设计,支持字符串、Map、自定义模型等任意数据类型,通用性极强。 - 优先级设计:自定义刷新指示器优先级高于原生,自定义状态文本样式优先级高于默认样式,既保证通用性又支持个性化。
- 资源安全释放 :在
dispose中释放ScrollController,避免内存泄漏,尤其在长列表和频繁切换页面场景中至关重要。
五、避坑指南(实际开发必看)
- 列表项高度一致性 :确保列表项高度统一或使用
MediaQuery动态适配,避免滚动监听时maxScrollExtent计算不准,导致加载更多触发异常。 - 初始数据去重 :
initialData建议提前去重,避免与下拉刷新 / 上拉加载的新数据重复,影响用户体验。 - 加载回调异常处理 :
onRefresh和onLoadMore中需自行处理接口异常(如网络错误、数据解析失败),组件仅捕获异常并切换为错误状态。 - 禁用功能适配 :禁用下拉刷新 / 上拉加载时,确保
onRefresh/onLoadMore返回合理值(如空列表),避免空指针异常。 - 长列表性能优化 :在
ListView.builder中使用cacheExtent控制预加载范围,列表项复杂时建议使用RepaintBoundary避免过度绘制。
总结
PullRefreshList组件通过 "通用化封装 + 精细化控制",彻底解决了原生列表开发的痛点,实现了 "一行代码搭建功能完整的列表"。无论是商品列表、消息流,还是公告、新闻列表,都能通过简单配置快速实现,既保证了交互体验的一致性,又大幅减少重复编码,让开发者聚焦业务逻辑而非基础交互。