flutter 刷新、加载与占位图一站式服务(基于easy_refresh扩展)

1,993 阅读5分钟

前文

今天聊到的是滚动视图刷新与加载,对于这个老生常谈的问题解决方案就太多了,优秀的插件也屡见不鲜,譬如 pull_to_refresheasy_refresh、还有笔者前段时间使用过的 infinite_scroll_pagination 这些都是极具代表性的优秀插件。 那你在使用时有没有类似情况:

  • 为了重复的样版式代码感到厌倦?
  • 为了Ctrl V+C感到无聊?
  • 看到通篇类似的代码想大刀阔斧的整改?
  • 更有甚者有没有本来只想自定义列表样式,却反而浪费更多的时间来完成基础配置?

现在我们来解决这类问题,欢迎来到走近科学探索发现 - Approaching Scientific Exploration and Discovery。(可能片场走错了:) )

注意

  • 本文以 easy_refresh 作为刷新框架举例说明(其他框架的类似)
  • 本文案例demo以 getx 作为项目状态管理框架(与刷新无关仅作为项目基础框架构成,不喜勿喷)

正文

现在请出我们重磅成员mixin,对于这个相信大家已经非常熟悉了。我们要做的就是利用easy_refresh提供的refresh controller对视图逻辑进行拆分,从而精简我们的样板式代码。

1、提炼逻辑,刷新控制与分页请求

首页拆离我们刷新和加载方法:

import 'dart:async';
import 'package:easy_refresh/easy_refresh.dart';
import 'state.dart';

mixin PagingMixin<T> {
  /// 刷新控制器
  final EasyRefreshController _pagingController = EasyRefreshController(
      controlFinishLoad: true, controlFinishRefresh: true);
  EasyRefreshController get pagingController => _pagingController;

  /// 初始页码      <---- 单独提出这个的原因是有时候我们请求的起始页码不固定,有可能是0,也有可能是1
  int _initPage = 0;

  /// 当前页码
  int _page = 0;
  int get page => _page;

  /// 列表数据
  List<T> get items => _state.items;
  int get itemCount => items.length;

  /// 错误信息
  dynamic get error => _state.error;

  /// 关联刷新状态管理的控制器       <----  自定义状态类型,后文会有阐述主要包含列表数据、初始加载是否为空、错误信息
  PagingMixinController get state => _state;
  final PagingMixinController<T> _state = PagingMixinController(
    PagingMixinData(items: []),
  );

  /// 是否加载更多       <----  可以在控制器中初始化传入,控制是否可以进行加载
  bool _isLoadMore = true;
  bool get isLoadMore => _isLoadMore;

  /// 控制刷新结束回调(异步处理)       <----  手动结束异步操作,并返回结果
  Completer? _refreshComplater;

  /// 挂载分页器
  /// `controller` 关联刷新状态管理的控制器
  /// `initPage` 初始页码值(分页起始页)
  /// `isLoadMore` 是否加载更多
  void initPaging({
    int initPage = 0,
    isLoadMore = true,
  }) {
    _isLoadMore = isLoadMore;
    _initPage = initPage;
    _page = initPage;
  }

  /// 获取数据
  FutureOr fetchData(int page);

  /// 刷新数据
  Future onRefresh() async {
    _refreshComplater = Completer();
    _page = _initPage;
    fetchData(_page);
    return _refreshComplater!.future;
  }

  /// 加载更多数据
  Future onLoad() async {
    _refreshComplater = Completer();
    _page++;
    fetchData(_page);
    return _refreshComplater!.future;
  }

  /// 获取数据后调用
  /// `items` 列表数据
  /// `maxCount` 数据总数,如果为0则默认通过 `items` 有无数据判断是否可以分页加载, null为非分页请求
  /// `error` 错误信息
  /// `limit` 单页显示数量限制,如果items.length < limit 则没有更多数据
  void endLoad(
    List<T>? list, {
    int? maxCount,
    // int limit = 5,
    dynamic error,
  }) {
    if (_page == _initPage) {
      _refreshComplater?.complete();
      _refreshComplater = null;
    }

    final dataList = List.of(_state.value.items);
    if (list != null) {
      if (_page == _initPage) {
        dataList.clear();
        // 更新数据
        _pagingController.finishRefresh();
        _pagingController.resetFooter();
      }
      dataList.addAll(list);
      // 更新列表
      _state.value = _state.value.copyWith(
        items: dataList,
        isStartEmpty: page == _initPage && list.isEmpty,
      );

      // 默认没有总数量 `maxCount`,用获取当前数据列表是否有值判断
      // 默认有总数量 `maxCount`, 则判断当前请求数据list+历史数据items是否小于总数
      // bool hasNoMore = !((items.length + list.length) < maxCount);
      bool isNoMore = true;
      if (maxCount != null) {
        isNoMore = page > 1; // itemCount >= maxCount;
      }
      var state = IndicatorResult.success;
      if (isNoMore) {
        state = IndicatorResult.noMore;
      }
      _pagingController.finishLoad(state);
    } else {
      _state.value = _state.value.copyWith(items: [], error: error ?? '数据请求错误');
    }
  }

}

创建PagingMixin<T>混入类型,泛型<T>属于列表子项的数据类型

void initPaging(...):初始化的时候可以写入基本设置(可以不调用)

Future onRefresh() Future onLoad():供外部调用的刷新加载方法

FutureOr fetchData(int page):由子类集成重写,主要是完成数据获取方法,在获取到数据后,需要调用方法void endLoad(...)来结束整个请求操作,通知视图刷新

PagingMixinController继承自ValueNotifier,是对数据相关状态的缓存,便于独立逻辑操作与数据状态:

    class PagingMixinController<T> extends ValueNotifier<PagingMixinData<T>> {
      PagingMixinController(super.value);

      dynamic get error => value.error;
      List<T> get items => value.items;
      int get itemCount => items.length;
    }
    // flutter 关于easy_refresh更便利的打开方式
    class PagingMixinData<T> {
      // 列表数据
      final List<T> items;

      /// 错误信息
      final dynamic error;

      /// 首次加载是否为空
      bool isStartEmpty;

      PagingMixinData({
        required this.items,
        this.error,
        this.isStartEmpty = false,
      });
      
      ....
      
      }

完成这两个类的编写,我们对于逻辑部分的拆离已经完成了。

2、简化使用,刷新框架的封装

下面是对easy_refresh的使用,封装:

    class PullRefreshControl extends StatelessWidget {
      const PullRefreshControl({
        super.key,
        required this.pagingMixin,
        required this.childBuilder,
        this.header,
        this.footer,
        this.locatorMode = false,
        this.refreshOnStart = true,
        this.startRefreshHeader,
      });

      final Header? header;
      final Footer? footer;

      final bool refreshOnStart;
      final Header? startRefreshHeader;

      /// 列表视图
      final ERChildBuilder childBuilder;

      /// 分页控制器
      final PagingMixin pagingMixin;

      /// 是否固定刷新偏移
      final bool locatorMode;

      @override
      Widget build(BuildContext context) {
        final firstRefreshHeader = startRefreshHeader ??
            BuilderHeader(
              triggerOffset: 70,
              clamping: true,
              position: IndicatorPosition.above,
              processedDuration: Duration.zero,
              builder: (ctx, state) {
                if (state.mode == IndicatorMode.inactive ||
                    state.mode == IndicatorMode.done) {
                  return const SizedBox();
                }
                return Container(
                  padding: const EdgeInsets.only(bottom: 100),
                  width: double.infinity,
                  height: state.viewportDimension,
                  alignment: Alignment.center,
                  child: SpinKitFadingCube(
                    size: 25,
                    color: Theme.of(context).primaryColor,
                  ),
                );
              },
            );

        return EasyRefresh.builder(
          controller: pagingMixin.pagingController,
          header: header ??
              RefreshHeader(
                clamping: locatorMode,
                position: locatorMode
                    ? IndicatorPosition.locator
                    : IndicatorPosition.above,
              ),
          footer: footer ?? const ClassicFooter(),
          refreshOnStart: refreshOnStart,
          refreshOnStartHeader: firstRefreshHeader,
          onRefresh: pagingMixin.onRefresh,
          onLoad: pagingMixin.isLoadMore ? pagingMixin.onLoad : null,
          childBuilder: (context, physics) {
            return ValueListenableBuilder(
              valueListenable: pagingMixin.state,
              builder: (context, value, child) {
                if (value.isStartEmpty) {
                  return _PagingStateView(
                    isEmpty: value.isStartEmpty,
                    onLoading: pagingMixin.onRefresh,
                  );
                }
                return childBuilder.call(context, physics);
              },
            );
          },
        );
      }
    }

创建PullRefreshControl类型,设置必须属性pagingMixinchildBuilder,前者是我们创建的PagingMixin对象(可以是任何类型,只要支持混入就可以了),后者是对我们滚动列表的实现。 其他的都是对 easy_refresh的属性配置,参考相关文档就行了。

到这里我们减配版的封装就完成了,使用方式如下:

截图

截图

3、分门别类,进一步简化

但是我们并没有完成我们前文所说的简化操作,还是需要一遍又一遍创建重复的滚动列表,所以我们继续:

/// 快速构建 `ListView` 形式的分页列表
/// 其他详细参数查看 [ListView]
class SpeedyPagedList<T> extends StatelessWidget {
  const SpeedyPagedList({
    super.key,
    required this.controller,
    required this.itemBuilder,
    this.scrollController,
    this.padding,
    this.header,
    this.footer,
    this.locatorMode = false,
    this.refreshOnStart = true,
    this.startRefreshHeader,
    double? itemExtent,
  })  : _separatorBuilder = null, 
        _itemExtent = itemExtent;

  const SpeedyPagedList.separator({
    super.key,
    required this.controller,
    required this.itemBuilder,
    required IndexedWidgetBuilder separatorBuilder,
    this.scrollController,
    this.padding,
    this.header,
    this.footer,
    this.locatorMode = false,
    this.refreshOnStart = true,
    this.startRefreshHeader,
  })  : _separatorBuilder = separatorBuilder,
        _itemExtent = null;

  final PagingMixin<T> controller;

  final Widget Function(BuildContext context, int index, T item) itemBuilder;

  final Header? header;
  final Footer? footer;

  final bool refreshOnStart;
  final Header? startRefreshHeader;
  final bool locatorMode;

  /// 参照 [ScrollView.controller].
  final ScrollController? scrollController;

  /// 参照 [ListView.itemExtent].
  final EdgeInsetsGeometry? padding;

  /// 参照 [ListView.separator].
  final IndexedWidgetBuilder? _separatorBuilder;

  /// 参照 [ListView.itemExtent].
  final double? _itemExtent;

  @override
  Widget build(BuildContext context) {
    return PullRefreshControl(
      pagingMixin: controller,
      header: header,
      footer: footer,
      refreshOnStart: refreshOnStart,
      startRefreshHeader: startRefreshHeader,
      locatorMode: locatorMode,
      childBuilder: (context, physics) {
        return _separatorBuilder != null
            ? ListView.separated(
                physics: physics,
                padding: padding,
                controller: scrollController,
                itemCount: controller.itemCount,
                itemBuilder: (context, index) {
                  final item = controller.items[index];
                  return itemBuilder.call(context, index, item);
                },
                separatorBuilder: _separatorBuilder!,
              )
            : ListView.builder(
                physics: physics,
                padding: padding,
                controller: scrollController,
                itemExtent: _itemExtent,
                itemCount: controller.itemCount,
                itemBuilder: (context, index) {
                  final item = controller.items[index];
                  return itemBuilder.call(context, index, item);
                },
              );
      },
    );
  }
}

...

归纳我们所需要的使用方式(我这里只写了ListView/GridView),构创建快速初始化加载列表的方法,将我们仅需要的Widget Function(BuildContext context, int index, T item) itemBuilder单个元素的创建(因为对于大多列表来说我们仅关心单个元素样式)暴露出来,简化PullRefreshControl的使用。

截图

对比前面的使用方式,现在更加简洁了,总计代码也就十几行吧。

4、总结

到这里就结束啦,文章也仅算是对繁杂重复使用的东西进行一些归纳总结,没有特别推崇的意思,更优秀的方案也比比皆是,所以仁者见仁了各位。

附GIF展示:

GIF 2023-6-9 14-23-45.gif

附Demo地址: boomcx/template_getx