在 Flutter 开发中,列表(商品列表、消息列表、订单列表)是高频场景。原生RefreshIndicator仅支持下拉刷新,上拉加载需手动监听滚动、管理加载状态,且空数据、错误等异常状态需重复开发。本文封装的CommonRefreshList整合 “下拉刷新 + 上拉加载 + 空状态 + 错误状态 + 加载中状态” 五大核心能力,支持分页逻辑、自定义状态样式,一行代码集成,覆盖 95%+ 列表场景,彻底解放重复编码!
一、核心优势(精准解决开发痛点)
✅ 状态全适配:内置加载中、空数据、错误、无更多数据 4 种异常状态,无需手动判断切换✅ 刷新加载整合:下拉刷新与上拉加载逻辑封装,无需单独处理滚动监听和状态管理✅ 分页逻辑内置:支持页码 / 游标分页,自动管理pageIndex和hasMore状态,减少重复代码✅ 高扩展性:下拉刷新样式、上拉加载提示、各状态页面均可自定义,适配不同设计风格✅ 性能优化:列表项复用、加载状态防重复触发(避免多次请求),适配大数据列表✅ 交互友好:错误状态支持点击重试、下拉刷新动画流畅、上拉加载触发阈值合理,贴合用户习惯
二、核心配置速览(关键参数一目了然)
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | itemBuilder、onLoadData | 列表项构建器(渲染单个列表项)、数据加载回调(分页请求数据) |
| 刷新配置 | enablePullDown、onRefresh | 是否启用下拉刷新(默认 true)、自定义刷新回调(优先级高于默认逻辑) |
| 加载配置 | enablePullUp、pageSize、hasMore | 是否启用上拉加载(默认 true)、每页数据量(默认 10)、是否有更多数据(外部控制) |
| 状态配置 | emptyWidget、errorWidget等 | 空数据、错误、加载中、无更多数据的自定义组件(支持个性化设计) |
| 列表配置 | controller、itemExtent、padding | 滚动控制器(外部监听滚动)、列表项固定高度(优化性能)、内边距 |
三、生产级完整代码(可直接复制,开箱即用)
dart
import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; /// 列表加载状态枚举(统一管理所有状态,逻辑清晰) enum ListLoadStatus { loading, // 初始加载中 empty, // 空数据 error, // 加载错误 success, // 加载成功(有数据) noMore // 上拉加载无更多 } /// 通用列表刷新加载组件(支持下拉刷新、上拉加载、多状态适配) class CommonRefreshList<T> extends StatefulWidget { // 必选参数(核心依赖) final Widget Function(BuildContext, T, int) itemBuilder; // 列表项构建器(context, 数据, 索引) final Future<List<T>> Function(int pageIndex, int pageSize) onLoadData; // 数据加载回调(页码, 页大小) // 刷新配置(下拉刷新相关) final bool enablePullDown; // 是否启用下拉刷新(默认true) final Future<void> Function()? onRefresh; // 自定义下拉刷新回调(优先级高于默认逻辑) final Color refreshColor; // 刷新指示器颜色(默认蓝色) final double refreshTriggerDistance; // 下拉刷新触发距离(默认100px) // 加载配置(上拉加载相关) final bool enablePullUp; // 是否启用上拉加载(默认true) final int pageSize; // 每页数据量(默认10) final int initialPageIndex; // 初始页码(默认1) final bool hasMore; // 是否有更多数据(外部控制,默认true) final String loadMoreText; // 上拉加载提示文本(默认"正在加载更多...") final String noMoreText; // 无更多数据提示文本(默认"没有更多数据了") final TextStyle loadTextStyle; // 加载提示文本样式(默认14号灰色) // 状态配置(各异常状态组件) final Widget? loadingWidget; // 初始加载中组件(自定义样式) final Widget? emptyWidget; // 空数据组件(自定义样式) final Widget? errorWidget; // 错误组件(点击可重试,自定义样式) final Widget? noMoreWidget; // 无更多数据组件(自定义样式) // 列表配置(基础样式与性能优化) final ScrollController? controller; // 滚动控制器(外部传入可监听滚动位置) final ScrollPhysics? physics; // 滚动物理效果(默认适配平台) final EdgeInsetsGeometry? padding; // 列表内边距(默认无) final double? itemExtent; // 列表项固定高度(优化滚动性能,推荐设置) final bool shrinkWrap; // 是否适应子组件高度(默认false,避免列表高度异常) const CommonRefreshList({ super.key, required this.itemBuilder, required this.onLoadData, // 刷新配置 this.enablePullDown = true, this.onRefresh, this.refreshColor = Colors.blue, this.refreshTriggerDistance = 100.0, // 加载配置 this.enablePullUp = true, this.pageSize = 10, this.initialPageIndex = 1, this.hasMore = true, this.loadMoreText = "正在加载更多...", this.noMoreText = "没有更多数据了", this.loadTextStyle = const TextStyle(fontSize: 14, color: Colors.grey), // 状态配置 this.loadingWidget, this.emptyWidget, this.errorWidget, this.noMoreWidget, // 列表配置 this.controller, this.physics, this.padding, this.itemExtent, this.shrinkWrap = false, }); @override State<CommonRefreshList<T>> createState() => _CommonRefreshListState<T>(); } class _CommonRefreshListState<T> extends State<CommonRefreshList<T>> { late ScrollController _scrollController; // 滚动控制器(复用外部传入或新建) late List<T> _dataList; // 列表数据源 late int _currentPage; // 当前页码 late ListLoadStatus _loadStatus; // 列表加载状态 bool _isLoadingMore = false; // 上拉加载锁(防重复请求) bool _isRefreshing = false; // 下拉刷新锁(防重复请求) @override void initState() { super.initState(); // 初始化滚动控制器(外部传入则复用,内部新建则自行管理生命周期) _scrollController = widget.controller ?? ScrollController(); // 初始化数据与状态 _dataList = []; _currentPage = widget.initialPageIndex; _loadStatus = ListLoadStatus.loading; // 监听滚动事件(触发上拉加载) _scrollController.addListener(_onScroll); // 初始加载数据 _initLoadData(); } @override void didUpdateWidget(covariant CommonRefreshList<T> oldWidget) { super.didUpdateWidget(oldWidget); // 外部控制hasMore变化时,恢复加载状态(支持重新加载更多) if (widget.hasMore != oldWidget.hasMore && _loadStatus == ListLoadStatus.noMore) { setState(() => _loadStatus = ListLoadStatus.success); } } @override void dispose() { // 外部传入的控制器由外部管理,内部新建的需手动释放 if (widget.controller == null) _scrollController.dispose(); super.dispose(); } /// 初始加载数据(首次进入页面触发) Future<void> _initLoadData() async { try { final data = await widget.onLoadData(_currentPage, widget.pageSize); setState(() { _dataList = data; // 根据返回数据判断状态:空数据→empty,有数据→success _loadStatus = data.isEmpty ? ListLoadStatus.empty : ListLoadStatus.success; }); } catch (e) { setState(() => _loadStatus = ListLoadStatus.error); EasyLoading.showError("加载失败:${e.toString()}"); } } /// 下拉刷新逻辑(重置页码,重新加载第一页) Future<void> _handleRefresh() async { if (_isRefreshing) return; // 防重复刷新 _isRefreshing = true; try { _currentPage = widget.initialPageIndex; // 重置页码 final newData = await widget.onLoadData(_currentPage, widget.pageSize); setState(() { _dataList = newData; _loadStatus = newData.isEmpty ? ListLoadStatus.empty : ListLoadStatus.success; }); } catch (e) { EasyLoading.showError("刷新失败:${e.toString()}"); } finally { _isRefreshing = false; // 释放刷新锁 } } /// 上拉加载逻辑(页码+1,追加数据) Future<void> _handleLoadMore() async { // 防重复加载:正在加载中/无更多数据/非成功状态→不触发 if (_isLoadingMore || !widget.hasMore || _loadStatus != ListLoadStatus.success) return; _isLoadingMore = true; try { _currentPage++; // 页码自增 final newData = await widget.onLoadData(_currentPage, widget.pageSize); setState(() { if (newData.isEmpty) { _loadStatus = ListLoadStatus.noMore; // 无更多数据 } else { _dataList.addAll(newData); // 追加新数据 } }); } catch (e) { _currentPage--; // 加载失败回退页码(避免跳过当前页) EasyLoading.showError("加载更多失败:${e.toString()}"); } finally { _isLoadingMore = false; // 释放加载锁 } } /// 滚动监听(判断是否触发上拉加载) void _onScroll() { if (!widget.enablePullUp) return; // 滚动到列表底部100px内,且未在加载中→触发加载更多 if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100 && !_isLoadingMore) { _handleLoadMore(); } } /// 重试加载(错误状态点击触发) void _onRetry() { setState(() => _loadStatus = ListLoadStatus.loading); _initLoadData(); } /// 构建初始加载中组件(默认+自定义适配) Widget _buildLoadingWidget() { return widget.loadingWidget ?? const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(strokeWidth: 2), SizedBox(height: 16), Text("正在加载中...", style: TextStyle(color: Colors.grey)), ], ), ); } /// 构建空数据组件(默认+自定义适配) Widget _buildEmptyWidget() { return widget.emptyWidget ?? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.inbox_outlined, size: 64, color: Colors.grey[300]), const SizedBox(height: 16), const Text("暂无数据", style: TextStyle(color: Colors.grey, fontSize: 16)), ], ), ); } /// 构建错误组件(默认+自定义适配,支持点击重试) Widget _buildErrorWidget() { return widget.errorWidget ?? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: Colors.grey[300]), const SizedBox(height: 16), const Text("加载失败", style: TextStyle(color: Colors.grey, fontSize: 16)), const SizedBox(height: 8), TextButton( onPressed: _onRetry, child: const Text("点击重试"), ), ], ), ); } /// 构建无更多数据组件(默认+自定义适配) Widget _buildNoMoreWidget() { return widget.noMoreWidget ?? Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Center(child: Text(widget.noMoreText, style: widget.loadTextStyle)), ); } /// 构建上拉加载提示组件(加载中状态) Widget _buildLoadMoreWidget() { return 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.loadMoreText, style: widget.loadTextStyle), ], ), ), ); } /// 构建列表主体(包含正常列表项+加载更多/无更多提示) Widget _buildListBody() { return ListView.builder( controller: _scrollController, physics: widget.physics, padding: widget.padding, itemExtent: widget.itemExtent, // 固定高度优化性能 shrinkWrap: widget.shrinkWrap, // 列表项数量=数据量+1(最后一项显示加载更多/无更多) itemCount: _dataList.length + (widget.enablePullUp ? 1 : 0), itemBuilder: (context, index) { // 最后一项:显示加载更多或无更多 if (widget.enablePullUp && index == _dataList.length) { return _loadStatus == ListLoadStatus.noMore ? _buildNoMoreWidget() : _buildLoadMoreWidget(); } // 正常列表项:通过itemBuilder渲染 return widget.itemBuilder(context, _dataList[index], index); }, ); } @override Widget build(BuildContext context) { // 根据加载状态显示对应页面 Widget child; switch (_loadStatus) { case ListLoadStatus.loading: child = _buildLoadingWidget(); break; case ListLoadStatus.empty: child = _buildEmptyWidget(); break; case ListLoadStatus.error: child = _buildErrorWidget(); break; case ListLoadStatus.success: case ListLoadStatus.noMore: child = _buildListBody(); break; } // 包裹下拉刷新组件(启用时) if (widget.enablePullDown) { child = RefreshIndicator( color: widget.refreshColor, triggerMode: RefreshIndicatorTriggerMode.onEdge, displacement: widget.refreshTriggerDistance, onRefresh: widget.onRefresh ?? _handleRefresh, // 优先使用自定义刷新逻辑 child: child, ); } return child; } }四、三大高频场景实战示例(直接复制可用)
场景 1:基础分页列表(商品列表,支持下拉刷新 + 上拉加载)
适用场景:电商商品列表、资讯列表等需要分页加载的场景
dart
class ProductListPage extends StatefulWidget { @override State<ProductListPage> createState() => _ProductListPageState(); } class _ProductListPageState extends State<ProductListPage> { bool _hasMore = true; // 控制是否有更多商品 // 商品数据模型(实际项目可替换为真实模型) class Product { final String id; final String name; final double price; final String imageUrl; Product({required this.id, required this.name, required this.price, required this.imageUrl}); } // 模拟加载商品数据(实际项目替换为接口请求) Future<List<Product>> _loadProductData(int pageIndex, int pageSize) async { await Future.delayed(const Duration(1000)); // 模拟网络延迟 // 模拟第3页无更多数据 if (pageIndex >= 3) { setState(() => _hasMore = false); return []; } // 生成模拟数据 return List.generate(pageSize, (index) { final realIndex = (pageIndex - 1) * pageSize + index; return Product( id: "prod_$realIndex", name: "2025新款夏季T恤 $realIndex", price: 99.0 + realIndex * 10, imageUrl: "https://picsum.photos/200/200?random=$realIndex", ); }); } // 构建商品列表项 Widget _buildProductItem(BuildContext context, Product product, int index) { return Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[200]!), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 商品图片 ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network( product.imageUrl, width: 80, height: 80, fit: BoxFit.cover, ), ), const SizedBox(width: 12), // 商品信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 8), Text( "¥${product.price.toStringAsFixed(2)}", style: const TextStyle(fontSize: 16, color: Colors.redAccent), ), ], ), ), // 加入购物车按钮 IconButton( icon: const Icon(Icons.add_shopping_cart, color: Colors.grey), onPressed: () => EasyLoading.showToast("添加 ${product.name} 到购物车"), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("商品列表")), body: CommonRefreshList<Product>( itemBuilder: _buildProductItem, onLoadData: _loadProductData, hasMore: _hasMore, pageSize: 8, // 每页8条数据 enablePullDown: true, enablePullUp: true, padding: const EdgeInsets.symmetric(vertical: 8), // 自定义空状态组件(贴合商品场景) emptyWidget: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.shopping_bag_outlined, size: 64, color: Colors.grey[300]), SizedBox(height: 16), Text("暂无商品数据", style: TextStyle(color: Colors.grey, fontSize: 16)), SizedBox(height: 8), Text("换个关键词试试吧~", style: TextStyle(color: Colors.grey[500], fontSize: 14)), ], ), ), ), ); } }场景 2:无分页列表(消息列表,仅下拉刷新)
适用场景:消息列表、通知列表等无需分页,仅需下拉刷新的场景
dart
class MessageListPage extends StatefulWidget { @override State<MessageListPage> createState() => _MessageListPageState(); } class _MessageListPageState extends State<MessageListPage> { // 消息数据模型 class Message { final String id; final String title; final String content; final String time; final bool isRead; // 是否已读 Message({ required this.id, required this.title, required this.content, required this.time, this.isRead = false, }); } // 加载消息数据(无分页,仅下拉刷新) Future<List<Message>> _loadMessageData(int pageIndex, int pageSize) async { await Future.delayed(const Duration(800)); // 模拟网络延迟 // 生成模拟消息数据 return List.generate(15, (index) { return Message( id: "msg_$index", title: index % 3 == 0 ? "系统通知" : "好友消息", content: "这是一条测试消息内容,用于展示列表项样式 $index", time: "${10 + index}:${index * 5}", isRead: index > 5, // 前5条为未读 ); }); } // 构建消息列表项 Widget _buildMessageItem(BuildContext context, Message message, int index) { return ListTile( leading: CircleAvatar( child: Text(message.title.substring(0, 1)), backgroundColor: message.isRead ? Colors.grey[200] : Colors.blue, foregroundColor: message.isRead ? Colors.grey : Colors.white, ), title: Text( message.title, style: TextStyle( fontWeight: message.isRead ? FontWeight.normal : FontWeight.bold, ), ), subtitle: Text( message.content, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: message.isRead ? Colors.grey : Colors.black87, ), ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(message.time, style: const TextStyle(fontSize: 12, color: Colors.grey)), // 未读红点 if (!message.isRead) const SizedBox( width: 8, height: 8, child: CircleAvatar(backgroundColor: Colors.red), ), ], ), onTap: () => EasyLoading.showToast("查看消息:${message.title}"), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("消息列表")), body: CommonRefreshList<Message>( itemBuilder: _buildMessageItem, onLoadData: _loadMessageData, enablePullDown: true, enablePullUp: false, // 关闭上拉加载(无分页) refreshColor: Colors.orangeAccent, // 自定义刷新颜色 // 自定义错误状态组件 errorWidget: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.message_outlined, size: 64, color: Colors.grey[300]), const SizedBox(height: 16), const Text("消息加载失败", style: TextStyle(color: Colors.grey, fontSize: 16)), const SizedBox(height: 8), ElevatedButton( onPressed: () {}, child: const Text("重新加载"), ), ], ), ), ), ); } }场景 3:固定高度列表(订单列表,优化滚动性能)
适用场景:订单列表、账单列表等列表项高度固定的场景(性能更优)
dart
class OrderListPage extends StatefulWidget { @override State<OrderListPage> createState() => _OrderListPageState(); } class _OrderListPageState extends State<OrderListPage> { bool _hasMore = true; // 控制是否有更多订单 // 订单数据模型 class Order { final String orderNo; final double amount; final String status; final String time; Order({ required this.orderNo, required this.amount, required this.status, required this.time, }); } // 加载订单数据 Future<List<Order>> _loadOrderData(int pageIndex, int pageSize) async { await Future.delayed(const Duration(1000)); // 模拟网络延迟 // 模拟第4页无更多数据 if (pageIndex >= 4) { setState(() => _hasMore = false); return []; } // 生成模拟订单数据 return List.generate(pageSize, (index) { final realIndex = (pageIndex - 1) * pageSize + index; final statusList = ["待支付", "已支付", "已取消", "已完成"]; return Order( orderNo: "ORDER${DateTime.now().year}${10000 + realIndex}", amount: 50.0 + realIndex * 20, status: statusList[realIndex % 4], time: "2024-12-${10 + realIndex % 20} 1${realIndex % 9}:${realIndex % 59}", ); }); } // 构建订单状态标签 Widget _buildStatusTag(String status) { Color color; switch (status) { case "待支付": color = Colors.orangeAccent; break; case "已支付": color = Colors.green; break; case "已取消": color = Colors.grey; break; case "已完成": color = Colors.blue; break; default: color = Colors.grey; } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: Text( status, style: TextStyle(color: color, fontSize: 12), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("订单列表")), body: CommonRefreshList<Order>( // 构建订单列表项 itemBuilder: (context, order, index) { return Container( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 订单号和状态 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("订单号:${order.orderNo}", style: const TextStyle(fontSize: 14, color: Colors.grey)), _buildStatusTag(order.status), ], ), const SizedBox(height: 8), // 订单金额 Text("订单金额:¥${order.amount.toStringAsFixed(2)}", style: const TextStyle(fontSize: 16)), const SizedBox(height: 8), // 下单时间和操作 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(order.time, style: const TextStyle(fontSize: 14, color: Colors.grey)), TextButton( onPressed: () => EasyLoading.showToast("查看订单详情:${order.orderNo}"), child: const Text("查看详情"), ), ], ), ], ), ); }, onLoadData: _loadOrderData, hasMore: _hasMore, pageSize: 6, itemExtent: 140, // 固定列表项高度(大幅提升滚动性能) padding: const EdgeInsets.symmetric(vertical: 8), // 自定义无更多数据组件 noMoreWidget: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Center( child: Text("已加载全部订单", style: widget.loadTextStyle.copyWith(fontSize: 15)), ), ), ), ); } }五、核心封装技巧(复用成熟设计思路)
- 状态分层管理:通过
ListLoadStatus枚举统一管理 5 种状态,避免分散判断,状态切换逻辑清晰,外部无需关心内部状态流转。 - 防重复触发机制:通过
_isLoadingMore和_isRefreshing加载锁,防止滚动或下拉时多次触发请求,避免接口压力和数据错乱。 - 分页逻辑解耦:页码管理、数据追加、无更多判断等逻辑内置,外部仅需实现
onLoadData回调返回数据,无需重复编写分页逻辑。 - 组件插槽化设计:各状态页面(空、错误、加载中)支持外部自定义,兼顾通用性和个性化,适配不同 APP 设计风格。
- 性能优化细节:支持
itemExtent固定列表项高度,减少 ListView 布局计算;复用外部传入的ScrollController,便于监听滚动位置实现吸顶等扩展功能。 - 错误恢复机制:上拉加载失败时自动回退页码,错误状态支持点击重试,提升用户体验,避免数据丢失。
六、避坑指南(解决 90% 开发痛点)
- 数据状态同步:
hasMore需外部根据接口返回结果更新(如无更多数据时设为false),否则会持续触发上拉加载。 - 页码回退关键:上拉加载失败时必须回退
_currentPage,否则下次加载会跳过当前页,导致数据断层。 - 控制器生命周期:外部传入的
ScrollController需由外部管理dispose,内部新建的控制器会自动释放,避免内存泄漏。 - 空状态适配:初始加载为空时显示空状态,下拉刷新后数据为空也会切换为空状态,需确保
onLoadData返回空列表时状态正确切换。 - 性能优化建议:列表项高度固定时务必设置
itemExtent,大数据列表(超过 50 项)建议配合ListView.builder复用特性,避免卡顿。 - 自定义刷新逻辑:如需自定义下拉刷新(如清除缓存),可通过
onRefresh回调实现,优先级高于默认逻辑,灵活适配特殊需求。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。