Flutter 通用下拉刷新上拉加载列表:PullRefreshList

在 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),
);

四、核心封装技巧(让组件更稳定、更易用)

  1. 状态锁防重复请求 :通过LoadStatus枚举控制状态,加载中时拦截下拉刷新和上拉加载的重复请求,避免接口并发调用导致的数据错乱。
  2. 提前触发加载更多:滚动到距离底部 200px 时触发加载,而非完全滚动到底部,减少用户等待感,提升交互体验。
  3. 泛型适配任意数据 :采用泛型<T>设计,支持字符串、Map、自定义模型等任意数据类型,通用性极强。
  4. 优先级设计:自定义刷新指示器优先级高于原生,自定义状态文本样式优先级高于默认样式,既保证通用性又支持个性化。
  5. 资源安全释放 :在dispose中释放ScrollController,避免内存泄漏,尤其在长列表和频繁切换页面场景中至关重要。

五、避坑指南(实际开发必看)

  1. 列表项高度一致性 :确保列表项高度统一或使用MediaQuery动态适配,避免滚动监听时maxScrollExtent计算不准,导致加载更多触发异常。
  2. 初始数据去重initialData建议提前去重,避免与下拉刷新 / 上拉加载的新数据重复,影响用户体验。
  3. 加载回调异常处理onRefreshonLoadMore中需自行处理接口异常(如网络错误、数据解析失败),组件仅捕获异常并切换为错误状态。
  4. 禁用功能适配 :禁用下拉刷新 / 上拉加载时,确保onRefresh/onLoadMore返回合理值(如空列表),避免空指针异常。
  5. 长列表性能优化 :在ListView.builder中使用cacheExtent控制预加载范围,列表项复杂时建议使用RepaintBoundary避免过度绘制。

总结

PullRefreshList组件通过 "通用化封装 + 精细化控制",彻底解决了原生列表开发的痛点,实现了 "一行代码搭建功能完整的列表"。无论是商品列表、消息流,还是公告、新闻列表,都能通过简单配置快速实现,既保证了交互体验的一致性,又大幅减少重复编码,让开发者聚焦业务逻辑而非基础交互。

https://openharmonycrossplatform.csdn.net/content

相关推荐
程序员Ctrl喵7 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难9 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡10 小时前
flutter列表中实现置顶动画
flutter
始持10 小时前
第十二讲 风格与主题统一
前端·flutter
始持10 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持10 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜11 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴11 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区12 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎12 小时前
树形选择器组件封装
前端·flutter