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

相关推荐
帅气马战的账号2 小时前
开源鸿蒙+Flutter:分布式协同驱动的全场景跨端开发新范式
flutter
帅气马战的账号2 小时前
开源鸿蒙+Flutter进阶实战:跨端融合与原生能力无缝调用新方案
flutter
晚霞的不甘2 小时前
Flutter + OpenHarmony 自动化测试全攻略:从单元测试到多设备真机云测
flutter·单元测试
ujainu2 小时前
Flutter性能优化实战:从卡顿到丝滑的全方案
flutter·性能优化
克喵的水银蛇2 小时前
Flutter 通用网络图片加载组件:ImageLoaderWidget 解决加载痛点
flutter
寒季6662 小时前
Flutter 智慧零售门店服务平台:跨端协同打造全渠道消费体验
flutter
解局易否结局2 小时前
Flutter:重构跨平台开发的技术范式与实践路径
flutter·重构
雨季6662 小时前
Flutter 智慧零售服务平台:跨端协同打造全链路消费生态
flutter·零售
雨季6662 小时前
Flutter 智慧零售服务平台:跨端协同打造全链路消费生态(精简版)
flutter·零售