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

相关推荐
2分钟速写快排12 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby606113 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角14 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!14 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰14 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi15 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen16 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马17 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒17 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端
乘风gg17 小时前
DeepSeek V4.1 Flash 来了,明天中午 Flash 降价 60%
前端·ai编程·claude