Flutter之上拉下拉 2024-10-17 周四

简介

下拉刷新,上拉加载更多,是一个比较常见的需求。而这个一般是和表格之类的组件一起出现的。
说实话,和弹窗和Toast等类似,要自己写的话比较麻烦,所以一般采用插件比较方便。

easy_refresh

在这里,我们选取的插件是easy_refresh,总体感觉还是可以的


pub

样式

  • 分为头部和底部样式
  • 可以设置成全局一致的
  // 全局设置
  EasyRefresh.defaultHeaderBuilder = () => RefreshUtils.header();
  EasyRefresh.defaultFooterBuilder = () => RefreshUtils.footer();
  • 除非特殊要求,一般用插件给的ClassicXXX就可以了,提醒文字
class RefreshUtils {
  static header() {
    return ClassicHeader(
      textStyle: const TextStyle(color: Color(0xff666666)),
      messageStyle: const TextStyle(color: Color(0xff666666)),
      iconTheme: const IconThemeData(color: Color(0xff666666)),
      dragText: '拉取刷新'.tr,
      armedText: '准备就绪'.tr,
      readyText: '刷新。。。'.tr,
      processingText: '刷新。。。'.tr,
      processedText: '刷新成功!'.tr,
      noMoreText: '没数据了'.tr,
      failedText: '失败'.tr,
      messageText: '${'上次更新'.tr} %T',
      mainAxisAlignment: MainAxisAlignment.end,
    );
  }

  static footer() {
    return ClassicFooter(
      textStyle: const TextStyle(color: Color(0xff666666)),
      messageStyle: const TextStyle(color: Color(0xff666666)),
      iconTheme: const IconThemeData(color: Color(0xff666666)),
      dragText: '拉到加载'.tr,
      armedText: '准备就绪'.tr,
      readyText: '加载中...'.tr,
      processingText: '加载中...'.tr,
      processedText: '加载成功!'.tr,
      noMoreText: '没数据了'.tr,
      failedText: '失败'.tr,
      messageText: '${'上次更新'.tr} %T',
      infiniteOffset: 70, // 这里给null就要主动上拉
    );
  }
}

基础组件

一般需要上拉下拉的都是表格之类的,性能问题比较突出。并且每个页面都要套一个EasyRefresh.builder也比较麻烦,所以干脆做一个通用组件比较合适。大家都用同一套东西。

  @override
  Widget build(BuildContext context) {
    return GetBuilder<OtherHomeLogic>(builder: (_) {
      return Scaffold(
        body: EasyRefresh.builder(
          header: RefreshUtils.header(),
          footer: RefreshUtils.footer(),
          onRefresh: logic.onRefresh,
          onLoad: logic.onLoad,
          childBuilder: (context, physics) {
            return CustomScrollView(
              physics: physics,
              slivers: [
                headWidget() ,
                contentWidget(),
              ],
            );
          },
        ),
        backgroundColor: StyleUtils.backgroundColor4,
      );
    });
  }

  Widget headWidget() {
    return SliverVisibility(
      visible: true,
      sliver: SliverAppBar(),
    );
  }

  Widget contentWidget() {
    return SliverToBoxAdapter(
      child: Column(
        children: [],
      ),
    );
  }

这里只是一个封装思路:头部有可能不显示,所以包在一个Visibility组件中,用参数控制(头部组件都可以放出一些参数配置)。至于内容部分,放在一个Column组件中,比sliver家族用起来方便很多。

EasyRefreshController

  • 这个就是上拉下拉的控制器。可以为nil,默认也是不控制。
  EasyRefreshController({
    this.controlFinishRefresh = false,
    this.controlFinishLoad = false,
  });
  • 大多数情况下,默认情况就可以了,下拉和上拉的时候有一定的提示,一段时间之后,上拉和下拉的提示就结束,然后显示上拉或者下拉成功的状态。

  • 假如需要上拉和下拉要反映接口的实际状态,那么就要引入EasyRefreshController,进行控制。简单讲就是在接口结束的时候调用finishRefresh或者finishLoad方法,结束加载的状态。

  • 状态定义:成功和失败很好立即。这个noMore比较特殊,是上拉加载特有的。设定这个状态之后,load方法就不会再触发了。

/// The status returned after the task is completed.
enum IndicatorResult {
  /// No state until the task is not triggered.
  none,

  /// Task succeeded.
  success,

  /// Task failed.
  fail,

  /// No more data.
  noMore,
}
  • 这个none状态也比较特殊,reset方法就是把noMore状态设置为none状态。猜测他的作用就是让上拉加载重新出发load方法的调用。
  /// Reset Footer indicator state.
  void resetFooter() {
    _state?._footerNotifier._reset();
  }

  /// Reset partial state, e.g. no more.
  void _reset() {
    if (_result == IndicatorResult.noMore) {
      _result = IndicatorResult.none;
    }
  }

分页的两种方式

  • size参数一般都是需要的,就是每次调用返回的数据量。

  • 一种方式是page参数,也就是当前页码,从1开始往上增长。这种比较常见。

  • 另外一种是lastId,就是上次数据的最后一个元素的Id。这种比较少见,不过我们这次的确是遇到了。

如何复用

  • 不复用的话,至少size和page或者lastId,还有EasyRefreshController这几个参数都是要重复定义的。

  • EasyRefresh本身就是组件,里面嵌入一个List之类的滑动组件就有上拉和下拉的状态。

  • 如果要实现上拉,下拉的状态控制,就要一个额外的EasyRefreshController参数,游离在组件之外,所以封装成Widget的方式比较困难。

  • 由于我们使用GetX,每个页面都有一个GetxController。所以可以考虑采用继承的方式,将上拉下拉这些基础变量做在这个基类之中。

基类的设计

变量

  • 我们要同时支持page和lastId两种方式,所以这3个变量是至少的。

  • 由于一个页面中,有可能只用一种方式,也有可能同时用两种方式,也有可能一种都不用,所以引入两个变量usePage和useLastId来控制。

  • 由于lastId是最后一个数据元素的id,所以需要一个变量来保存数据,就用List,数据类型就是字典,和约定好字段,比如id之类。我们直接取名叫dataList

  • 上拉加载,是否有更多数据,这个需要一个变量来保存,我们就取名为hasMore

  • 结束是成功还是失败,并且上拉和下拉分开,这样又引入两个变量refreshSuccess,loadSuccess

  • 还要区分一下上拉还是下拉,所以再引入一个变量isRefresh

/// GetX中,与page配套使用的logic
class RefreshGet extends GetxController {
  int page = 1;
  int size = 20;
  String lastId = '';
  bool usePage = true;
  bool useLastId = true;
  List dataList = [];
  bool hasMore = true;
  bool refreshSuccess = true;
  bool loadSuccess = true;
  bool isRefresh = true;

  /// 控制器
  EasyRefreshController refreshController = EasyRefreshController(
      controlFinishRefresh: true, controlFinishLoad: true);

  @override
  void onClose() {
    refreshController.dispose();

    super.onClose();
  }
}

上拉下拉

EasyRefresh组件需要上拉和下拉函数,这个定义在基类中就可以了。

  Future onRefresh() async {
    isRefresh = true;
    lastId = '';
    dataList = [];
    page = 1;
    hasMore = true;
    usePage = true;
    useLastId = true;
    refreshSuccess = true;
    loadSuccess = true;
    resetFooterAndHeader();
    refreshFunc();
  }

  Future onLoad() async {
    isRefresh = false;
    if (useLastId && dataList.isNotEmpty) {
      lastId = dataList.last['id'] ?? "";
    }
    if (usePage) {
      page++;
    }
    resetFooter();
    loadFunc();
  }

  void refreshFunc() {}

  void loadFunc() {}
  • 这里的refreshFunc和loadFunc是方便方法。在子类中,只要重写他们,直接把上拉和下拉的接口函数塞这里就可以了。

  • 重置是为了取消noMore状态,让上拉加载重新调用onLoad函数。

  /// 重置,将noMore状态改为none
  void resetFooterAndHeader() {
    resetFooter();
    resetHeader();
  }

  void resetFooter() {
    refreshController.resetFooter();
  }

  void resetHeader() {
    refreshController.resetHeader();
  }

状态设置

简单讲就是在接口函数结束之后,设置上拉和下拉的状态,提示数据的状态。

  /// 手动更新refresh或者load状态;isAll表示两者都做
  void onFinish({bool isAll = false}) {
    if (isAll) {
      updateHeader();
      updateFooter();
    } else {
      if (isRefresh) {
        updateHeader();
      } else {
        updateFooter();
      }
    }
  }

  /// 更新头部
  void updateHeader() {
    resetHeader();
    if (refreshSuccess) {
      refreshController.finishRefresh(IndicatorResult.success);
    } else {
      refreshController.finishRefresh(IndicatorResult.fail);
    }
  }

  /// 更新底部
  void updateFooter() {
    resetFooter();
    if (hasMore) {
      if (loadSuccess) {
        refreshController.finishLoad(IndicatorResult.success);
      } else {
        if (usePage && (page > 1)) {
          page--;
        }
        refreshController.finishLoad(IndicatorResult.fail);
      }
    } else {
      refreshController.finishLoad(IndicatorResult.noMore);
    }
  }

设置结果

接口调用是否成功,需要进行记录。一般上拉和下拉不同时进行,所以需要分别保存

  /// 设置结果标志,上拉下拉分开保存。isAll表示两个标记存成一样的
  void updateResult(bool isSuccess, {bool isAll = false}) {
    if (isAll) {
      refreshSuccess = isSuccess;
      loadSuccess = isSuccess;
    } else {
      if (isRefresh) {
        refreshSuccess = isSuccess;
      } else {
        loadSuccess = isSuccess;
      }
    }
  }

如何使用

  • 在界面文件中用上Controller,onRefresh和onLoad函数
        body: SafeArea(
          child: EasyRefresh.builder(
              controller: logic.refreshController,
              header: RefreshUtils.header(),
              footer: RefreshUtils.footer(),
              onRefresh: logic.onRefresh,
              onLoad: logic.onLoad,
              childBuilder: (context, physics) {
                return CustomScrollView(
                  controller: _controller,
                  physics: physics,
                  slivers: [
                    mapModule(),
                    everyoneBrowsing(),
                    everyoneDiscuss(),
                  ],
                );
              }),
        ),
  • 对应的logic文件继承我们的基类RefreshGet
class HomeLogic extends RefreshGet { }
  • 重写空方法refreshFunc和loadFunc,指定具体的上拉和下拉动作。由于基类中是空方法,可以省略super的调用。当然,写上也没问题。
  @override
  void refreshFunc() {
    super.refreshFunc();

    /// 这里用lastId的方式
    usePage = false;
    useLastId = true;

    /// 刷新函数;只有getThemeListFeeds是分页函数;getHotGameList是页面的其他部分
    getHotGameList();
    getThemeListFeeds();
  }

  @override
  void loadFunc() {
    super.loadFunc();
    
    /// 分页函数
    getThemeListFeeds();
  }

改写分页函数

需要分页的函数与普通函数不一样,需要改写相关的分页参数。

  /// 获取主题列表feeds
  void getThemeListFeeds() async {
    if (!hasMore) {
      LogUtil.log("getThemeListFeeds没有更多了,直接返回");
      onFinish();
      update();
      return;
    }
    ApiResponse response = await HomeApi.getThemeListFeeds({'size': size, 'lastId': lastId});
    if (response.code == 0) {
      /// 标记成功
      updateResult(true);

      /// 追加本次分页数据
      List data = response.data ?? [];
      if (data.isNotEmpty) {
        dataList.addAll(data);
      }

      /// 如果本次返回的数据少于size,说明没有更多了
      if (data.length < size) {
        hasMore = false;
      } else {
        hasMore = true;
      }
    } else {
      /// 标记失败
      updateResult(false);
    }

    /// 更新上拉下拉组件状态
    onFinish();

    /// 更新页面
    update();
  }

小结

  • 将上拉下拉需要的一些变量和方法下沉到基类中,在实际使用的时候,相对还是比较简单的。如果偷懒一点usePage和useLastId这两个变量都不用设置,保持true就可以了。page不断自增,反正又不用,也没有关系。反之,lastId不用的话,一直设置为空字符串,放着也没关系。

  • 如果id的字段不是“id”,其实关系也不大,只要在refreshFunc方法中重新设置一下lastId就可以了

  • 如果想用默认的上拉下拉状态,只要在EasyRefresh不设置controller参数就可以了。基类中多一个refreshController变量的创建和销毁,无伤大雅。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容