Flutter 进阶:最佳实践 —— 刷新列表组件化 NRefreshListView

一、需求来源

项目中有很多很多的列表,一直在研究如何组件化列表,但是按照最初的想法实现后发现有这样或那样的缺点。最近灵光一闪,在原先基础上做了减法和扩展,基本能满足大部分的开发场景,分享给大家。因为牵扯到业务就没有动图演示了,直接上代码。

二、使用示例

dart 复制代码
...

/// 方案列表
class SchemeListPage extends StatefulWidget {

  const SchemeListPage({
    super.key,
    this.arguments,
  });

  final Map<String, dynamic>? arguments;

  @override
  State<SchemeListPage> createState() => _SchemeListPageState();
}

class _SchemeListPageState extends State<SchemeListPage> {

  /// 获取上个页面传的参数
  /// userId --- 用户id
  late Map<String, dynamic> arguments = widget.arguments ?? Get.arguments;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("$widget"),
        actions: ['done',].map((e) => TextButton(
          child: Text(e,
            style: TextStyle(color: Colors.white),
          ),
          onPressed: () => debugPrint(e),)
        ).toList(),
      ),
      body: buildBody(),
    );
  }

  buildBody() {
    return NRefreshListView<DepartmentPageDetailModel>(
      pageSize: 2,
      onRequest: (bool isRefresh, int page, int pageSize, last) async {

        return await requestList(pageNo: page, pageSize: pageSize);
      },
      itemBuilder: (BuildContext context, int index, e) {

        return InkWell(
          onTap: () {
            YLog.d("${e.toJson()}");
          },
          child: SchemeCell(
            model: e,
            index: index,
          ),
        );
      },
    );
  }

  /// 列表数据请求
  Future<List<DepartmentPageDetailModel>> requestList({
    required int pageNo,
    int pageSize = 20,
  }) async {
    var api = SchemePageApi(
      ownerId: arguments['userId'] ?? '',
      pageNo: pageNo,
      pageSize: pageSize,
    );

    Map<String, dynamic>? response = await api.startRequest();
    if (response['code'] != 'OK') {
      return [];
    }

    final rootModel = DepartmentPageRootModel.fromJson(response ?? {});
    var list = rootModel.result?.content ?? [];
    return list;
  }
}

三、源码

kotlin 复制代码
//
//  NRefreshListView.dart
//  flutter_templet_project
//
//  Created by shang on 2024/3/8 10:59.
//  Copyright © 2024/3/8 shang. All rights reserved.
//


import 'package:easy_refresh/easy_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/util/color_util_new.dart';
import 'package:flutter_templet_projectp/widget/n_placeholder.dart';
import 'package:flutter_templet_project/widget/n_skeleton_screen.dart';


typedef ValueIndexedWidgetBuilder<T> = Widget Function(
    BuildContext context, int index, T data);

/// 请求列表回调
typedef RequestListCallback<T> = Future<List<T>> Function(
    bool isRefresh, int pageNo, int pageSize, T? last,);

/// 刷新列表组件化
class NRefreshListView<T> extends StatefulWidget {
  const NRefreshListView({
    super.key,
    this.controller,
    this.child,
    required this.onRequest,
    this.onRequestError,
    this.pageSize = 20,
    this.pageNoInitial = 1,
    this.disableOnReresh = false,
    this.disableOnLoad = false,
    this.needRemovePadding = false,
    required this.itemBuilder,
    this.separatorBuilder,
    this.errorBuilder,
    this.cachedChild,
    this.refreshController,
  });
 
  /// 控制器
  final NRefreshListViewController<T>? controller;
 
  /// 子视图(为空 默认 带刷新组件的 ListView)
  final Widget? child;

  /// 刷新页面不变的部分,
  final Widget? cachedChild;

  /// 每页数量
  final int pageSize;

  /// 页面初始索引
  final int pageNoInitial;

  /// 禁用下拉刷新
  final bool disableOnReresh;

  /// 禁用上拉加载
  final bool disableOnLoad;

  /// 使用使用 MediaQuery.removePadding
  final bool needRemovePadding;

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

  /// 请求错误方法
  final void Function(Object error, StackTrace stack)? onRequestError;

  /// 错误视图构建器
  final TransitionBuilder? errorBuilder;

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

  /// ListView 的 separatorBuilder
  final IndexedWidgetBuilder? separatorBuilder;

  /// 刷新控制器
  final EasyRefreshController? refreshController;


  @override
  NRefreshListViewState<T> createState() => NRefreshListViewState<T>();
}

class NRefreshListViewState<T> extends State<NRefreshListView<T>> {
  late final _easyRefreshController = widget.refreshController ??
      EasyRefreshController(
        controlFinishRefresh: true,
        controlFinishLoad: true,
      );

  final _scrollController = ScrollController();

  var indicator = IndicatorResult.none;

  late var pageNo = widget.pageNoInitial;

  late final items = ValueNotifier(<T>[]);

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

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

    widget.onRequest(true, pageNo, widget.pageSize, null).then((value) {
      items.value = value;
      isFirstLoad = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (isFirstLoad) {
      return const NSkeletonScreen();
    }

    return buildBody();
  }

  buildBody() {
    return ValueListenableBuilder<List<T>>(
      valueListenable: items,
      builder: (context, list, child) {
      
        if (list.isEmpty) {
          return NPlaceholder(
            onTap: onLoad,
          );
        }

        return buildRefresh(
          child: widget.child ??
              buildListView(
                controller: _scrollController,
                needRemovePadding: widget.needRemovePadding,
                items: list,
              ),
        );
      },
    );
  }

  Widget buildRefresh({
    Widget? child,
  }) {
    return EasyRefresh(
      controller: _easyRefreshController,
      triggerAxis: Axis.vertical,
      onRefresh: widget.disableOnReresh ? null : () => onRefresh(),
      onLoad: widget.disableOnLoad || indicator == IndicatorResult.noMore
          ? null
          : () => onLoad(),
      child: child,
    );
  }

  onRefresh() async {
    pageNo = widget.pageNoInitial;
    items.value = await widget.onRequest(true, pageNo, widget.pageSize, null);
    indicator = items.value.length < widget.pageSize
        ? IndicatorResult.noMore
        : IndicatorResult.success;
    _easyRefreshController.finishRefresh(indicator);
  }

  onLoad() async {
    if (!mounted) {
      return;
    }
    if (indicator == IndicatorResult.noMore) {
      return;
    }

    pageNo += 1;
    final models = await widget.onRequest(
        false,
        pageNo,
        widget.pageSize,
        items.value.isNotEmpty ? items.value.last : null);
    items.value = [...items.value, ...models];

    indicator = models.length < widget.pageSize
        ? IndicatorResult.noMore
        : IndicatorResult.success;
    _easyRefreshController.finishLoad(indicator);
  }

  Widget buildListView({
    ScrollController? controller,
    bool needRemovePadding = false,
    required List<T> items,
  }) {
    Widget child = Scrollbar(
      controller: controller,
      child: ListView.separated(
        controller: controller,
        itemCount: items.length,
        itemBuilder: (context, index) =>
            widget.itemBuilder(context, index, items[index]),
        separatorBuilder: widget.separatorBuilder ??
                (context, index) {
              return const Divider(
                color: Color(0xffe4e4e4),
                height: 1,
              );
            },
      ),
    );
    if (needRemovePadding) {
      child = MediaQuery.removePadding(
        removeTop: true,
        removeBottom: true,
        context: context,
        child: child,
      );
    }
    return child;
  }
}

/// NRefreshListView 组件控制器,将 NRefreshListViewState 的私有属性或者方法暴漏出去
class NRefreshListViewController<E> {

  NRefreshListViewState<E>? _anchor;

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

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


  void _attach(NRefreshListViewState<E> anchor) {
    _anchor = anchor;
  }

  void _detach(NRefreshListViewState<E> anchor) {
    if (_anchor == anchor) {
      _anchor = null;
    }
  }
}

四、总结

1、NRefreshListView 是构思了很长时间才研究出来的组件

使用之后一个列表只需要一个列表请求方法,一个 NRefreshListView 组件调用,代码极其简单,只需要创建一下模型,配置一下 api 请求参数即可。

从此刻开始,十分钟一个列表不是梦!

2、NRefreshListView 封装了
  • 下拉刷新,上拉加载;
  • 首屏加载有 NSkeletonScreen;
  • 数据为空有 NPlaceholder;
  • 通过 child 可传入任何滚动视图;
  • 通过 itemBuilder 将 ListView 的索引和模型透传到外部,子项随意定制;

github

相关推荐
红尘散仙6 小时前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
陈随易7 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
ZhengEnCi7 小时前
Agent 工具怎么折叠海量输出才「爽」:Trae、Claude Code 的渐进式披露设计原理
前端·agent
IT_陈寒10 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala10 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
ikoala11 小时前
AI 写页面像说明书,修 Bug 像贴膏药?把这两句话写进 AGENTS.md
前端·javascript·ai编程
好的佩奇11 小时前
Dart 之扩展方法
android·flutter·dart
去伪存真13 小时前
AI Agent Harness 深度测评与技术选型实战
前端·agent
好的佩奇14 小时前
蓝牙之经典蓝牙
android·flutter·蓝牙
FRing0315 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2