普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月30日首页

Flutter最佳实践:Sliver族网络刷新组件NCustomScrollView

作者 SoaringHeart
2026年1月30日 21:07

一、需求来源

最近需要实现嵌套和吸顶Header滚动下的下拉刷新及上拉加载。最终实现基于 CustomScrollView 的刷新视图组件。

simulator_screenshot_5C4883E4-F919-4FFD-BE3D-97E0BCD5C40D.png

二、使用示例

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 混入里,方便多组件可共用。

github

❌
❌