Flutter最佳实践:Sliver族网络刷新组件NCustomScrollView
2026年1月30日 21:07
一、需求来源
最近需要实现嵌套和吸顶Header滚动下的下拉刷新及上拉加载。最终实现基于 CustomScrollView 的刷新视图组件。
![]()
二、使用示例
Widget buildBodyNew() {
return NCustomScrollView<String>(
onRequest: (bool isRefresh, int page, int pageSize, pres) async {
final length = isRefresh ? 0 : pres.length;
final list = List<String>.generate(pageSize, (i) => "item${length + i}");
DLog.d([isRefresh, list.length]);
return list;
},
headerSliverBuilder: (context, bool innerBoxIsScrolled) {
return [
buildPersistentHeader(),
];
},
itemBuilder: (_, i, e) {
return ListTile(
title: Text('Item $i'),
);
},
);
}
三、源码
//
// 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/n_sliver_decorated.dart';
import 'package:flutter_templet_project/basicWidget/refresh/easy_refresh_mixin.dart';
import 'package:flutter_templet_project/basicWidget/refresh/n_refresh_view.dart';
/// 基于 CustomScrollView 的下拉刷新,上拉加载更多的滚动列表
class NCustomScrollView<T> extends StatefulWidget {
const NCustomScrollView({
super.key,
this.title,
this.placeholder = const NPlaceholder(),
this.contentDecoration = const BoxDecoration(),
this.contentPadding = const EdgeInsets.all(0),
required this.onRequest,
required this.headerSliverBuilder,
required this.itemBuilder,
this.separatorBuilder,
this.headerBuilder,
this.footerBuilder,
this.builder,
});
final String? title;
final Widget? placeholder;
final Decoration contentDecoration;
final EdgeInsets contentPadding;
/// 请求方法
final RequestListCallback<T> onRequest;
/// 列表表头
final NestedScrollViewHeaderSliversBuilder? headerSliverBuilder;
/// 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, EasyRefreshMixin<NCustomScrollView<T>, T> {
@override
bool get wantKeepAlive => true;
final scrollController = ScrollController();
@override
late RequestListCallback<T> onRequest = widget.onRequest;
@override
List<T> items = <T>[];
@override
void didUpdateWidget(covariant NCustomScrollView<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.title != oldWidget.title ||
widget.placeholder != oldWidget.placeholder ||
widget.contentDecoration != oldWidget.contentDecoration ||
widget.contentPadding != oldWidget.contentPadding ||
widget.onRequest != oldWidget.onRequest ||
widget.itemBuilder != oldWidget.itemBuilder ||
widget.separatorBuilder != oldWidget.separatorBuilder) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
super.build(context);
if (items.isEmpty) {
return GestureDetector(onTap: onRefresh, child: Center(child: widget.placeholder));
}
final child = EasyRefresh.builder(
controller: refreshController,
onRefresh: onRefresh,
onLoad: onLoad,
childBuilder: (_, physics) {
return CustomScrollView(
physics: physics,
slivers: [
...(widget.headerBuilder?.call(items.length) ?? []),
buildContent(),
...(widget.footerBuilder?.call(items.length) ?? []),
],
);
},
);
if (widget.headerSliverBuilder == null) {
return child;
}
return NestedScrollView(
headerSliverBuilder: widget.headerSliverBuilder!,
body: child,
);
}
Widget buildContent() {
if (items.isEmpty) {
return SliverToBoxAdapter(child: widget.placeholder);
}
return NSliverDecorated(
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,
);
}
}
源码:EasyRefreshMixin.dart
//
// EasyRefreshMixin.dart
// projects
//
// Created by shang on 2026/1/28 14:37.
// 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/refresh/n_refresh_view.dart';
/// EasyRefresh刷新 mixin
mixin EasyRefreshMixin<W extends StatefulWidget, T> on State<W> {
late final refreshController = EasyRefreshController(
controlFinishRefresh: true,
controlFinishLoad: true,
);
/// 请求方式
late RequestListCallback<T> _onRequest;
RequestListCallback<T> get onRequest => _onRequest;
set onRequest(RequestListCallback<T> value) {
_onRequest = value;
}
// 数据列表
List<T> _items = [];
List<T> get items => _items;
set items(List<T> value) {
_items = value;
}
int page = 1;
final int pageSize = 20;
var indicator = IndicatorResult.success;
@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();
}
});
}
Future<void> onRefresh() async {
try {
page = 1;
final list = await onRequest(true, page, pageSize, <T>[]);
items.replaceRange(0, items.length, list);
page++;
final noMore = list.length < pageSize;
if (noMore) {
indicator = IndicatorResult.noMore;
}
refreshController.finishRefresh();
refreshController.resetFooter();
} catch (e) {
refreshController.finishRefresh(IndicatorResult.fail);
}
setState(() {});
}
Future<void> onLoad() async {
if (indicator == IndicatorResult.noMore) {
refreshController.finishLoad();
return;
}
try {
final start = (items.length - pageSize).clamp(0, pageSize);
final prePages = items.sublist(start);
final list = await onRequest(false, page, pageSize, prePages);
items.addAll(list);
page++;
final noMore = list.length < pageSize;
if (noMore) {
indicator = IndicatorResult.noMore;
}
refreshController.finishLoad(indicator);
} catch (e) {
refreshController.finishLoad(IndicatorResult.fail);
}
setState(() {});
}
}
最后、总结
1、当页面比较复杂,需要吸顶或者嵌套滚动时就必须使用 Sliver 相关组件,否则会有滚动行文冲突。
2、NCustomScrollView 支持顶部吸顶组件自定义;底部列表头,列表尾设置,支持sliver 设置 Decoration。
3、支持下拉刷新,上拉加载更多,代码极简,使用方便。
4、刷新逻辑封装在 EasyRefreshMixin 混入里,方便多组件可共用。