一、需求来源
开发中中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 内部操作,可以自己扩展