如何封装一个 Flutter 列表刷新组件 —— 使用 EasyRefresh 实现分页加载、下拉刷新和上拉加载

29 阅读4分钟

在 Flutter 中,列表是应用中非常常见的一部分,尤其是在展示大量数据时。为了优化用户体验,我们通常需要支持分页加载、下拉刷新和上拉加载等功能。为了应对这些需求,我们可以封装一个通用的列表刷新组件,提供更灵活的自定义能力和更高效的代码复用。

本文将详细介绍如何使用 EasyRefresh 插件封装一个通用的分页刷新列表组件,包括实现分页加载、下拉刷新、上拉加载、空数据展示等功能,并展示如何在实际项目中使用。


一、背景需求分析

开发中我们常常会遇到列表数据较多的情况,加载全部数据可能会导致卡顿或耗费过多的网络带宽。为了避免这些问题,通常采用以下策略:

  1. 分页加载:将数据分成若干页,每次加载一部分数据,减少一次性加载的数据量。
  2. 下拉刷新:用户下拉时刷新列表,通常是为了获取最新的数据。
  3. 上拉加载:用户上拉时加载更多数据,通常用于分页加载更多列表项。
  4. 空数据展示:当列表没有数据时,展示一个空状态页面。

为了解决这些需求,我们需要设计一个通用的组件,支持分页加载、下拉刷新、上拉加载等功能,并能够根据实际需求自定义列表项的展示。


二、组件设计思路

我们的目标是封装一个高度可复用的列表组件,支持以下功能:

  • 分页加载:通过 pageNumpageSize 控制分页加载的数据。
  • 下拉刷新和上拉加载:利用 EasyRefresh 插件实现下拉刷新和上拉加载。
  • 自定义子组件:允许开发者自定义列表项的展示方式。
  • 空数据展示:当没有数据时,显示一个提示组件。

接下来,我们将通过代码逐步实现这些功能。


三、RefreshList 组件的代码实现

首先,我们实现一个 RefreshList 组件,它将处理分页加载、下拉刷新和上拉加载的逻辑。

import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';

typedef ApiCallback = Future<Map<String, dynamic>> Function(int pageNum, int pageSize);
typedef CustomChildBuilder = Widget Function(BuildContext context, List<dynamic> dataList);

class RefreshList extends StatefulWidget {
  final ApiCallback apiCallback; // API请求方法
  final Widget Function(BuildContext context, dynamic item)? itemBuilder; // 每一项的构建
  final Widget emptyWidget; // 空数据时的展示组件
  final CustomChildBuilder? childBuilder; // 自定义 child 构建方法

  const RefreshList({
    Key? key,
    required this.apiCallback,
    this.itemBuilder,
    this.emptyWidget = const Center(child: Text("暂无数据")),
    this.childBuilder,
  }) : super(key: key);

  @override
  RefreshListState createState() => RefreshListState();
}

class RefreshListState extends State<RefreshList> {
  int pageNum = 1; // 当前页数
  final int pageSize = 10; // 每页大小
  int totalPages = 1; // 总页数
  bool isLoading = false; // 是否正在加载
  List<dynamic> dataList = []; // 数据列表

  Future<void> fetchData({bool isRefresh = false}) async {
    if (isLoading) return; // 防止重复加载
    setState(() {
      isLoading = true;
    });

    try {
      final response = await widget.apiCallback(pageNum, pageSize);

      final dynamic rawData = response['list'] ?? [];
      final List<dynamic> newData =
          rawData is List ? rawData : [rawData]; // 转为列表

      setState(() {
        if (isRefresh) {
          dataList = newData;
        } else {
          dataList.addAll(newData);
        }
        totalPages = response['pages'] ?? 1; // 设置总页数
      });
    } catch (e) {
      debugPrint("加载数据失败: $e");
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  Future<void> onRefresh() async {
    dataList.clear();
    setState(() {
      pageNum = 1;
    });
    await fetchData(isRefresh: true);
  }

  Future<void> onLoad() async {
    if (pageNum >= totalPages) return;
    setState(() {
      pageNum++;
    });
    await fetchData();
  }

  @override
  void initState() {
    super.initState();
    fetchData(isRefresh: true);
  }

  @override
  Widget build(BuildContext context) {
    return EasyRefresh(
      header: BallPulseHeader(
        backgroundColor: Colors.white,
        color: Color(0xFF60D5C7),
      ),
      footer: BallPulseFooter(
        backgroundColor: Colors.white,
        color: Color(0xFF60D5C7),
      ),
      onRefresh: onRefresh,
      onLoad: onLoad,
      child: widget.childBuilder != null
          ? widget.childBuilder!(context, dataList)
          : (dataList.isEmpty
              ? widget.emptyWidget
              : ListView.builder(
                  itemCount: dataList.length,
                  itemBuilder: (context, index) {
                    return widget.itemBuilder!(context, dataList[index]);
                  },
                )),
    );
  }
}

组件解析

1. RefreshList 组件的构造函数

  • apiCallback:这个回调函数是我们向 API 请求数据的核心。它需要返回一个包含分页信息的数据结构(例如,listpages)。
  • itemBuilder:这是一个可选的回调函数,用来构建列表项。通过它可以自定义每一项的 UI。
  • emptyWidget:用于展示空数据时的提示组件,默认为一个简单的文本“暂无数据”。
  • childBuilder:如果传入了这个回调函数,RefreshList 会使用它来自定义构建整个列表,而不是默认的 ListView.builder

2. RefreshListState 状态类

  • fetchData:核心的 API 请求方法,负责获取数据并更新 dataList。该方法接受一个 isRefresh 参数,指示是否为下拉刷新操作。如果是刷新,它会清空原数据并重新加载;如果是加载更多,它会将新数据追加到现有数据列表中。
  • onRefreshonLoad:分别处理下拉刷新和上拉加载操作。
  • initState:初始化时自动请求第一页数据。

3. EasyRefresh 插件

EasyRefresh 插件提供了非常简单的 API 来实现下拉刷新和上拉加载功能。我们通过 BallPulseHeaderBallPulseFooter 自定义了下拉刷新和上拉加载时的动画效果。


四、如何在项目中使用 RefreshList 组件

在实际项目中,我们可以通过传入 API 请求函数来使用这个组件。以下是一个使用 RefreshList 组件的完整示例。

class MyPage extends StatelessWidget {
  // 模拟 API 请求
  Future<Map<String, dynamic>> fetchData(int pageNum, int pageSize) async {
    await Future.delayed(Duration(seconds: 2)); // 模拟网络延时
    return {
      'list': List.generate(pageSize, (index) => 'Item ${index + 1}'), // 返回模拟数据
      'pages': 5, // 模拟总页数
    };
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('分页加载示例')),
      body: RefreshList(
        apiCallback: fetchData,
        itemBuilder: (context, item) {
          return ListTile(title: Text(item));
        },
        emptyWidget: Center(child: Text("没有更多数据")),
      ),
    );
  }
}

关键点说明:

  • apiCallback:我们实现了 fetchData 方法,这个方法模拟了一个网络请求,返回了分页数据。你可以根据实际需求替换为真实的 API 调用。
  • itemBuilder:我们通过 itemBuilder 回调自定义了列表项的显示方式。在这个例子中,简单地使用了 ListTile 来展示每一项数据。
  • emptyWidget:当没有数据时,我们展示一个简单的文本提示“没有更多数据”。

五、总结

通过封装 RefreshList 组件,我们可以快速实现通用的分页加载、下拉刷新和上拉加载的功能,而不需要在每个页面中重复编写这些逻辑。这种方式的好处在于:

  • 复用性高:组件化的设计可以在多个页面中复用,减少代码冗余。
  • 可定制性强:通过传入不同的回调函数和展示方法,我们可以灵活地控制数据加载逻辑和 UI 展示。
  • 易于维护:将常见的列表操作封装到一个组件中,使得项目结构更加清晰,易于维护。

无论是在展示静态数据、动态加载内容,还是处理空数据情况,RefreshList 组件都能很好地满足这些需求,帮助开发者更专注于业务逻辑,而不是重复构建复杂的 UI 组件。