Flutter 组件抽取:分页加载列表控件(LoadListView)

该示例展示了如何在Flutter应用中使用EasyRefresh插件实现分页加载列表、下拉刷新和上拉加载更多功能。LoadListView类封装了接口调用,当加载数据长度小于每页数量时会停止加载更多。同时,提供了自定义UI效果的选项。
摘要由CSDN通过智能技术生成

简介

分页加载列表功能抽取,下拉刷新列表,上拉加载更多

效果

在这里插入图片描述

范例

class _TestPageState extends State<TestPage> {
  Future<List<String>?> loadData(int pageNum, int pageSize) async {
    await Future.delayed(const Duration(seconds: 2));
    return Random().nextBool()
        ? ['000']
        : ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
  }

  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('LoadListView')),
      body: LoadListView<String>(
        pageSize: 10,
        refreshOnStart: true,
        emptyWidget: const Center(
          child: Padding(
            padding: EdgeInsets.only(top: 100),
            child: Text('此处空空如也'),
          ),
        ),
        onRefreshData: (pageNum, pageSize) async {
          return loadData(pageNum, pageSize);
        },
        onLoadData: (pageNum, pageSize) async {
          return loadData(pageNum, pageSize);
        },
        itemBuilder: (context, count, index, data) {
          return Container(
            alignment: Alignment.center,
            color: Colors.blue.withOpacity(Random().nextInt(10) / 10.0),
            height: 100,
            child: Text(data),
          );
        },
      ),
    );
  }
}

说明

1、下拉刷新及上拉加载更多的接口由使用者自行对接
2、每次下拉刷新都会重置当前列表数据及分页配置
3、当上拉加载更多接口返回的数据长度小于每页加载数量时,后续上拉加载操作不再发起接口请求
4、集成 easy_refresh 插件,实现下拉、上拉效果,可进行下拉、上拉 UI 效果自定义(需要修改代码实现)

依赖

  # EasyRefresh:https://pub.dev/packages/easy_refresh
  easy_refresh: ^3.3.1

代码

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

typedef ListItemBuilder<T> = Widget Function(
    BuildContext context, int count, int index, T data);
typedef OnRefreshData<T> = Future<T> Function(int pageNum, int pageSize);
typedef OnLoadData<T> = Future<T> Function(int pageNum, int pageSize);
typedef OnDataUpdate<T> = void Function(T datas);

/// 分页加载 ListView
/// 根据每页加载数量与实际单次加载数量,自维护下拉加载更多操作:
/// 单次加载数量小于每页加载数量的话,禁用加载更多,只有重新下拉刷新,才能更新这个状态
///
/// 结合 EasyRefresh 实现下拉刷新及上拉加载更多,屏蔽 EasyRefresh 相关接口,这样有利于第三方解耦
/// 后续要自定义下拉和上拉UI时,要屏蔽 EasyRefresh 相关接口,不要强耦合
///
/// EasyRefresh接口文档:https://github.com/xuelongqy/flutter_easy_refresh/blob/v3/README_CN.md
///
class LoadListView<T> extends StatefulWidget {
  final int pageSize;
  final Widget? emptyWidget;
  final bool refreshOnStart;
  final bool canDropDown;
  final bool canPullUp;
  final OnRefreshData<List<T>?> onRefreshData;
  final OnLoadData<List<T>?> onLoadData;
  final ListItemBuilder<T> itemBuilder;
  final OnDataUpdate<List<T>>? onDataUpdate;
  final LoadListViewController? controller;

  /// [pageSize] 每页加载条目数,页数 pageNum 由 _LoadListViewState 维护
  /// [emptyWidget] 数据为空时的占位视图,靠上居中
  /// [refreshOnStart] 首次自动刷新
  /// [canDropDown] 是否可以下拉(刷新),默认true
  /// [canPullUp] 是否可以上拉(加载),默认true
  /// [itemBuilder] item视图构造器
  /// [onRefreshData] 下拉刷新数据,由使用者维护传入
  /// [onLoadData] 上拉加载更多数据,由使用者维护传入
  /// [onDataUpdate] 数据更新回调
  const LoadListView({
    Key? key,
    this.pageSize = 20,
    this.emptyWidget,
    this.refreshOnStart = false,
    this.canDropDown = true,
    this.canPullUp = true,
    required this.onRefreshData,
    required this.onLoadData,
    required this.itemBuilder,
    this.onDataUpdate,
    this.controller,
  }) : super(key: key);

  
  State<LoadListView<T>> createState() => _LoadListViewState<T>();
}

class _LoadListViewState<T> extends State<LoadListView<T>> {
  bool hasRefresh = false;
  List<T> dataList = [];
  int pageNum = 0;
  bool noMore = false;
  final EasyRefreshController refreshController = EasyRefreshController(
    controlFinishRefresh: true,
    controlFinishLoad: true,
  );
  final ScrollController scrollController = ScrollController();

  
  void setState(VoidCallback fn) {
    if (!mounted) return;
    super.setState(fn);
  }

  
  void initState() {
    super.initState();
    widget.controller?._setOnListener(
      getTotalCount: () => dataList.length,
      callRefresh: (bool silent) {
        if (silent) {
          refreshDataSilently();
        } else {
          refreshData();
        }
      },
      removeItem: (index) {
        if (dataList.length < (index + 1)) {
          debugPrint('删除item失败,数组越界');
          return;
        }
        dataList.removeAt(index);
        widget.onDataUpdate?.call(dataList);
        setState(() {});
      },
    );
    if (widget.refreshOnStart) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        refreshDataSilently();
      });
    }
  }

  
  void dispose() {
    refreshController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return EasyRefresh(
      callRefreshOverOffset: 5,
      controller: refreshController,
      header: const MaterialHeader(),
      footer: const ClassicFooter(
        triggerOffset: 40,
        showMessage: false,
        iconDimension: 0,
        iconTheme: IconThemeData(size: 0),
        spacing: 0,
        processingText: '正在加载',
        noMoreText: '没有更多',
        textStyle: TextStyle(color: Colors.grey, fontSize: 12),
      ),
      onRefresh: widget.canDropDown ? refreshDataSilently : null,
      onLoad: widget.canPullUp ? loadData : null,
      child: (dataList.isEmpty && hasRefresh && (widget.emptyWidget != null))
          ? SizedBox(
              height: double.maxFinite,
              child: SingleChildScrollView(
                controller: scrollController,
                child: widget.emptyWidget!,
              ),
            )
          : ListView.builder(
              controller: scrollController,
              itemCount: dataList.length,
              padding: const EdgeInsets.fromLTRB(0, 0, 0, 10),
              itemBuilder: (BuildContext context, int index) {
                debugPrint('+++ LoadListView index:$index +++');
                return widget.itemBuilder(
                    context, dataList.length, index, dataList[index]);
              },
            ),
    );
  }

  /// 刷新数据:展示下拉header
  Future<void> refreshData() async {
    refreshController.callRefresh();
  }

  /// 静默刷新数据:不展示下拉header,直接刷新数据
  Future<void> refreshDataSilently() async {
    debugPrint('+++ LoadListView refreshData +++');
    // 每次刷新就重置数据,然后加载第一页
    pageNum = 1;
    noMore = false;
    List<T> list = (await widget.onRefreshData(pageNum, widget.pageSize)) ?? [];
    if (!mounted) return;
    if (list.isEmpty) {
      pageNum = 0;
    }
    setState(() {
      hasRefresh = true;
      dataList = list;
      widget.onDataUpdate?.call(dataList);
    });
    // 下拉刷新加载的数据如果为空或者小于每页条目数,说明没有更多数据
    noMore = (list.length < widget.pageSize);
    refreshController.finishRefresh(IndicatorResult.success);

    /// 跳转到顶部:
    /// 用于解决下拉刷新数据为空时,采用[widget.emptyWidget]后,EasyRefresh下拉没有反应的问题
    if (list.isEmpty && (widget.emptyWidget != null)) {
      Future.delayed(const Duration(milliseconds: 100), () {
        if (!mounted) return;
        scrollController.jumpTo(0.00001);
      });
    }
  }

  /// 下拉加载更多数据
  Future<void> loadData() async {
    if (noMore) {
      refreshController.finishLoad(IndicatorResult.noMore);
      debugPrint('+++ LoadListView 没有更多 +++');
      return;
    }
    debugPrint('+++ LoadListView loadData +++');
    pageNum += 1;
    List<T> list = (await widget.onLoadData(pageNum, widget.pageSize)) ?? [];
    if (!mounted) return;
    if (list.isEmpty) {
      pageNum -= 1;
    } else {
      setState(() {
        dataList.addAll(list);
        widget.onDataUpdate?.call(dataList);
      });
    }
    // 最后一次加载的数据如果为空或者小于每页条目数,说明没有更多数据
    noMore = (list.length < widget.pageSize);
    refreshController
        .finishLoad(noMore ? IndicatorResult.noMore : IndicatorResult.none);
  }
}

class LoadListViewController {
  void _setOnListener({
    required Function? getTotalCount,
    required Function(bool silent)? callRefresh,
    required void Function(int index)? removeItem,
  }) {
    _getItemCount = getTotalCount;
    _callRefresh = callRefresh;
    _removeItem = removeItem;
  }

  Function? _getItemCount;

  /// 获取当前数据总数
  int get itemCount {
    return _getItemCount?.call() ?? 0;
  }

  Function(bool silent)? _callRefresh;

  /// 触发下拉刷新
  /// [silent] 是否静默刷新,不展示转圈圈,默认展示转圈圈
  void callRefresh({bool silent = false}) {
    _callRefresh?.call(silent);
  }

  void Function(int index)? _removeItem;

  void removeItem(int index) {
    _removeItem?.call(index);
  }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值