Flutter进阶:基于 EasyRefresh 的下拉刷新封装 n_easy_refresh_mixin.dart

一、需求来源

开发中中90%的页面需要下拉刷新功能,所以一直在构思能否有个完美的封装思路,将刷新通用逻辑封装。这样既可以提高开发效率,方便维护,也可以减少bug。经过多个app打磨,两次重大升级,现在到了比较满意的时候,分享给大家。

架构:

1、第一层 NRefreshable 为原生接口,仅提供子公共参数及方法。

dart 复制代码
abstract interface class NRefreshable 为原始刷新接口 {
  bool get isLoading;
  set isLoading(bool value);

  IndicatorResult get indicator;
  set indicator(IndicatorResult value);

  Future<void> onRefresh();

  Future<void> onLoad();

  /// 更新UI
  void updateUI();
}

2、第二层 基于 NRefreshable 创建新接口,补充不同场景必要参数及方法。

dart 复制代码
/// 列表页使用 mixin
mixin NListRefreshable<T> on NRefreshable {
  // 预置列表(弹窗类, 先请求第一页数据再显示页面)
  List<T> get firstPageItems;
  set firstPageItems(List<T> value);

  List<T> get items;
  set items(List<T> value);

  int get page;
  set page(int value);

  int get pageSize;
  set pageSize(int value);

  /// 更新数据源
  void updateItems(List<T> list);
}
dart 复制代码
/// 详情页使用
mixin NModelRefreshable<T> on NRefreshable {
  T? get item;
  set item(T? value);

  /// 更新数据源
  void updateItem(T v);
}

3、第三层基于第二层接口实现刷新逻辑(数据请求,界面刷新)。

dart 复制代码
/// EasyRefresh刷新 mixin, 控制器可用
mixin NListRefreshMixin<T> implements NListRefreshable<T> {
  var refreshController = EasyRefreshController(
    controlFinishRefresh: true,
    controlFinishLoad: true,
  );

  /// 请求方式
  late RequestListCallback<T> _onRequest;
  RequestListCallback<T> get onRequest => _onRequest;
  set onRequest(RequestListCallback<T> value) {
    _onRequest = value;
  }

  /// 数据列表
  @override
  List<T> items = [];

  @override
  List<T> firstPageItems = [];

  @override
  int page = 1;

  @override
  int pageSize = 20;

  @override
  var indicator = IndicatorResult.success;

  @override
  bool isLoading = false;

  bool get hasMore => indicator != IndicatorResult.noMore;

  @override
  Future<void> onRefresh() async {
    try {
      if (isLoading) {
        refreshController.finishRefresh();
        return;
      }
      isLoading = true;

      page = 1;
      final list = firstPageItems.isNotEmpty ? firstPageItems : await onRequest(true, page, pageSize, <T>[]);
      // items.replaceRange(0, items.length, list);
      items = [...list];
      page++;

      indicator = list.length < pageSize ? IndicatorResult.noMore : IndicatorResult.success;
      refreshController.finishRefresh();
      refreshController.resetFooter();
    } catch (e) {
      refreshController.finishRefresh(IndicatorResult.fail);
    } finally {
      isLoading = false;
      updateUI();
    }
  }

  @override
  Future<void> onLoad() async {
    if (indicator == IndicatorResult.noMore) {
      refreshController.finishLoad(indicator);
      return;
    }

    try {
      if (isLoading) {
        refreshController.finishLoad(indicator);
        return;
      }
      isLoading = true;

      final start = (items.length - pageSize).clamp(0, pageSize);
      final prePages = items.sublist(start);
      final list = await onRequest(false, page, pageSize, prePages);
      // items.addAll(list);
      items = [...items, ...list];
      page++;

      indicator = list.length < pageSize ? IndicatorResult.noMore : IndicatorResult.success;
      refreshController.finishLoad(indicator);
    } catch (e) {
      refreshController.finishLoad(IndicatorResult.fail);
    } finally {
      isLoading = false;
      updateUI();
    }
  }

  @override
  void updateItems(List<T> list) {
    items = [...list];
  }

  @override
  void updateUI() => throw UnimplementedError('updateUI');
}
dart 复制代码
/// EasyRefresh刷新 mixin, 控制器可用
mixin NModelRefreshMixin<T> implements NModelRefreshable<T> {
  var refreshController = EasyRefreshController(
    controlFinishRefresh: true,
    controlFinishLoad: true,
  );

  /// 请求方式
  late RequestModelCallback<T> _onRequest;
  RequestModelCallback<T> get onRequest => _onRequest;
  set onRequest(RequestModelCallback<T> value) {
    _onRequest = value;
  }

  /// 数据列表
  @override
  T? item;

  @override
  var indicator = IndicatorResult.success;

  @override
  bool isLoading = false;

  @override
  Future<void> onRefresh() async {
    try {
      if (isLoading) {
        refreshController.finishRefresh();
        return;
      }
      isLoading = true;

      item = await onRequest();
      indicator = item == null ? IndicatorResult.fail : IndicatorResult.success;
      refreshController.finishRefresh(indicator);
      refreshController.resetFooter();
    } catch (e) {
      refreshController.finishRefresh(IndicatorResult.fail);
    } finally {
      isLoading = false;
      updateUI();
    }
  }

  @override
  Future<void> onLoad() => throw UnimplementedError('onLoad');

  @override
  void updateItem(T v) {
    item = v;
    updateUI();
  }

  @override
  void updateUI() => throw UnimplementedError('updateUI');
}

和控制器类支持

dart 复制代码
class NListRefreshController<T> {
  NListRefreshable<T>? _anchor;

  void attach(NListRefreshable<T> anchor) {
    _anchor = anchor;
  }

  void detach(NListRefreshable<T> anchor) {
    if (_anchor == anchor) {
      _anchor = null;
    }
  }

  List<T> get items {
    assert(_anchor != null);
    return _anchor!.items;
  }

  void onRefresh() {
    assert(_anchor != null);
    _anchor!.onRefresh();
  }

  /// 页码减一
  void turnPrePage() {
    assert(_anchor != null);
    _anchor!.page--;
  }

  /// 页码加一
  void turnNextPage() {
    assert(_anchor != null);
    _anchor!.page++;
  }

  void updateItems(List<T> list) {
    assert(_anchor != null);
    _anchor!.updateItems(list);
  }

  void updateUI() {
    assert(_anchor != null);
    _anchor!.updateUI();
  }
}
dart 复制代码
/// 列表刷新控制器,配和 NModelRefreshable 使用
class NRefreshController<T> {
  NModelRefreshable<T>? _anchor;

  void attach(NModelRefreshable<T> anchor) {
    _anchor = anchor;
  }

  void detach(NModelRefreshable<T> anchor) {
    if (_anchor == anchor) {
      _anchor = null;
    }
  }

  T? get item {
    assert(_anchor != null);
    return _anchor!.item;
  }

  Future<void> onRefresh() {
    assert(_anchor != null);
    return _anchor!.onRefresh();
  }

  void updateItem(T v) {
    assert(_anchor != null);
    _anchor!.updateItem(v);
  }

  void updateUI() {
    assert(_anchor != null);
    _anchor!.updateUI();
  }
}

4、第四层基于第三层接口在 State 混入中实现所有刷新逻辑。

dart 复制代码
/// EasyRefresh刷新 mixin, StatefulWidget 列表页使用
mixin NListRefreshStateMixin<W extends StatefulWidget, T> on State<W>, NListRefreshMixin<T> {
  @override
  void dispose() {
    refreshController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      // DLog.d([widget.title, widget.key, hashCode]);
      if (items.isEmpty) {
        onRefresh();
      }
    });
  }

  @override
  void updateItems(List<T> list) {
    items.replaceRange(0, items.length, list);
    updateUI();
  }

  @override
  void updateUI() {
    setState(() {});
  }
}
dart 复制代码
/// EasyRefresh刷新 mixin, StatefulWidget 详情页使用
mixin NRefreshStateMixin<W extends StatefulWidget, T> on State<W>, NModelRefreshMixin<T> {
  @override
  void dispose() {
    refreshController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (item == null) {
        onRefresh();
      }
    });
  }

  @override
  void updateUI() {
    setState(() {});
  }
}

5、第五层基于第四层mixin 实现组件封装

列表页刷新组件

dart 复制代码
//
//  NCustomScrollView.dart
//  projects
//
//  Created by shang on 2026/1/28 14:41.
//  Copyright © 2026/1/28 shang. All rights reserved.
//

import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_placeholder.dart';
import 'package:flutter_templet_project/basicWidget/refresh/n_easy_refresh_mixin.dart';

/// 基于 CustomScrollView 的下拉刷新,上拉加载更多的滚动列表
/// 可以配合 NestedScrollView 添加吸顶组件使用
class NCustomScrollView<T> extends StatefulWidget {
  const NCustomScrollView({
    super.key,
    this.controller,
    this.scrollController,
    this.placeholder = const NPlaceholder(),
    this.contentDecoration = const BoxDecoration(),
    this.contentPadding = const EdgeInsets.all(0),
    this.onlyHeader = false,
    required this.onRequest,
    required this.itemBuilder,
    this.separatorBuilder,
    this.headerBuilder,
    this.footerBuilder,
    this.builder,
  });

  /// 刷新控制器
  final NListRefreshController<T>? controller;

  final ScrollController? scrollController;

  final Widget? placeholder;

  final Decoration contentDecoration;

  final EdgeInsets contentPadding;

  /// 列表为空时 header 是否可以显示
  final bool onlyHeader;

  /// 请求方法
  final RequestListCallback<T> onRequest;

  /// ListView 的 itemBuilder
  final ValueIndexedWidgetBuilder<T> itemBuilder;

  final IndexedWidgetBuilder? separatorBuilder;

  /// 列表表头
  final List<Widget> Function(int count)? headerBuilder;

  /// 列表表尾
  final List<Widget> Function(int count)? footerBuilder;

  final Widget Function(List<T> items)? builder;

  @override
  State<NCustomScrollView<T>> createState() => _NCustomScrollViewState<T>();
}

class _NCustomScrollViewState<T> extends State<NCustomScrollView<T>>
    with AutomaticKeepAliveClientMixin, NListRefreshMixin<T>, NListRefreshStateMixin<NCustomScrollView<T>, T> {
  @override
  bool get wantKeepAlive => true;

  final scrollController = ScrollController();

  @override
  late RequestListCallback<T> onRequest = widget.onRequest;

  @override
  void dispose() {
    widget.controller?.detach(this);
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    widget.controller?.attach(this);
  }

  @override
  void didUpdateWidget(covariant NCustomScrollView<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.scrollController != oldWidget.scrollController ||
        widget.placeholder != oldWidget.placeholder ||
        widget.contentDecoration != oldWidget.contentDecoration ||
        widget.contentPadding != oldWidget.contentPadding ||
        widget.onlyHeader != oldWidget.onlyHeader ||
        widget.onRequest != oldWidget.onRequest ||
        widget.itemBuilder != oldWidget.itemBuilder ||
        widget.separatorBuilder != oldWidget.separatorBuilder) {
      if (widget.controller != null && oldWidget.controller != widget.controller) {
        oldWidget.controller?.detach(this);
        widget.controller?.attach(this);
      }
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    if (items.isEmpty && !widget.onlyHeader) {
      return GestureDetector(onTap: onRefresh, child: Center(child: widget.placeholder));
    }

    return EasyRefresh.builder(
      controller: refreshController,
      onRefresh: onRefresh,
      onLoad: indicator == IndicatorResult.noMore ? null : onLoad,
      childBuilder: (_, physics) {
        return CustomScrollView(
          controller: widget.scrollController,
          physics: physics,
          slivers: [
            ...(widget.headerBuilder?.call(items.length) ?? []),
            buildContent(),
            ...(widget.footerBuilder?.call(items.length) ?? []),
          ],
        );
      },
    );
  }

  Widget buildContent() {
    if (items.isEmpty) {
      return SliverToBoxAdapter(child: Center(child: widget.placeholder));
    }

    return DecoratedSliver(
      decoration: widget.contentDecoration,
      sliver: SliverPadding(
        padding: widget.contentPadding,
        sliver: widget.builder?.call(items) ?? buildSliverList(),
      ),
    );
  }

  Widget buildSliverList() {
    return SliverList.separated(
      itemBuilder: (_, i) => widget.itemBuilder(context, i, items[i]),
      separatorBuilder: (_, i) => widget.separatorBuilder?.call(context, i) ?? const SizedBox(),
      itemCount: items.length,
    );
  }

  @override
  void updateUI() {
    setState(() {});
  }
}

详情页刷新组件

dart 复制代码
//
//  NCustomScrollViewOne.dart
//  projects
//
//  Created by shang on 2026/1/28 14:41.
//  Copyright © 2026/1/28 shang. All rights reserved.
//

import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_templet_project/basicWidget/n_placeholder.dart';
import 'package:flutter_templet_project/basicWidget/n_skeleton_screen.dart';
import 'package:flutter_templet_project/basicWidget/refresh/n_easy_refresh_mixin.dart';

/// 基于 CustomScrollView 的下拉刷新详情展示
/// 可以配合 NestedScrollView 添加吸顶组件使用
class NCustomScrollViewForModel<T> extends StatefulWidget {
  const NCustomScrollViewForModel({
    super.key,
    this.controller,
    this.scrollController,
    this.placeholder = const NPlaceholder(),
    this.skeletonScreen = const NSkeletonScreen(),
    this.contentDecoration = const BoxDecoration(),
    this.contentPadding = const EdgeInsets.all(0),
    this.onlyHeader = false,
    required this.onRequest,
    required this.builder,
    this.headerBuilder,
    this.footerBuilder,
  });

  /// 刷新控制器
  final NRefreshController<T>? controller;

  final ScrollController? scrollController;

  final Widget? placeholder;

  final Widget? skeletonScreen;

  final Decoration contentDecoration;

  final EdgeInsets contentPadding;

  /// 列表为空时 header 是否可以显示
  final bool onlyHeader;

  /// 请求方法
  final RequestModelCallback<T> onRequest;

  final Widget Function(BuildContext context, T? model) builder;

  /// 表头
  final List<Widget> Function(BuildContext context, T? m)? headerBuilder;

  /// 表尾
  final List<Widget> Function(BuildContext context, T? m)? footerBuilder;

  @override
  State<NCustomScrollViewForModel<T>> createState() => _NCustomScrollViewForModelState<T>();
}

class _NCustomScrollViewForModelState<T> extends State<NCustomScrollViewForModel<T>>
    with AutomaticKeepAliveClientMixin, NModelRefreshMixin<T>, NRefreshStateMixin<NCustomScrollViewForModel<T>, T> {
  @override
  bool get wantKeepAlive => true;

  late var scrollController = widget.scrollController ?? ScrollController();

  @override
  late RequestModelCallback<T> onRequest = widget.onRequest;

  /// 首次加载
  var isFirstLoad = true;

  @override
  void dispose() {
    widget.controller?.detach(this);
    refreshController.dispose();
    scrollController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    widget.controller?.attach(this);
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      if (item == null) {
        await onRefresh();
        isFirstLoad = false;
      }
    });
  }

  @override
  void didUpdateWidget(covariant NCustomScrollViewForModel<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller ||
        widget.scrollController != oldWidget.scrollController ||
        widget.placeholder != oldWidget.placeholder ||
        widget.contentDecoration != oldWidget.contentDecoration ||
        widget.contentPadding != oldWidget.contentPadding ||
        widget.onlyHeader != oldWidget.onlyHeader ||
        widget.onRequest != oldWidget.onRequest) {
      if (widget.controller != null && oldWidget.controller != widget.controller) {
        oldWidget.controller?.detach(this);
        widget.controller?.attach(this);
      }
      if (widget.scrollController != null) {
        scrollController = widget.scrollController!;
      }
      onRequest = widget.onRequest;
      setState(() {});
    }
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);

    if (isFirstLoad && widget.skeletonScreen != null) {
      return widget.skeletonScreen!;
    }

    if (item == null && !widget.onlyHeader) {
      return GestureDetector(onTap: onRefresh, child: Center(child: widget.placeholder));
    }

    return EasyRefresh.builder(
      controller: refreshController,
      scrollController: scrollController,
      onRefresh: onRefresh,
      onLoad: null,
      childBuilder: (_, physics) {
        return CustomScrollView(
          controller: scrollController,
          physics: physics,
          slivers: [
            ...(widget.headerBuilder?.call(context, item) ?? []),
            buildContent(),
            ...(widget.footerBuilder?.call(context, item) ?? []),
          ],
        );
      },
    );
  }

  Widget buildContent() {
    if (item == null) {
      return SliverToBoxAdapter(child: widget.placeholder);
    }

    return DecoratedSliver(
      decoration: widget.contentDecoration,
      sliver: SliverPadding(
        padding: widget.contentPadding,
        sliver: widget.builder(context, item),
      ),
    );
  }

  @override
  void updateUI() {
    setState(() {});
  }
}

最后、总结

1、Sliver组件适用性要好,适配场景更广,非 Sliver 组件使用使用更加方便。如果是复杂页面,推荐无脑上 NCustomScrollView/NCustomScrollViewForModel。

列表组件: NCustomScrollView(Sliver)和 NRefreshListView。

详情页组件: NCustomScrollViewForModel(Sliver)和 NRefreshView。

2、通过 NListRefreshController和 NRefreshController 控制器进行 state 内部操作,可以自己扩展

github

相关推荐
IT_陈寒3 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰3 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding
竹林8184 小时前
用 The Graph 查询链上数据实战:从手搓 RPC 到 Subgraph,我的 NFT 项目数据加载快了 10 倍
前端·javascript
妙码生花4 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十九):点选验证码代码逐行目检
前端·后端·go
Awu12275 小时前
⚡从零开发 Agent CLI(五)实现一个可治理、可扩展的工具系统
前端·人工智能·claude
咪库咪库咪5 小时前
Vue3-生命周期
前端
莪_幻尘6 小时前
你的 AI Skill 越多越蠢?Token 上下文爆炸的求生指南
前端·ai编程
lichenyang4536 小时前
从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘
前端
林瞅瞅6 小时前
Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案
前端