likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 6

一、需求来源

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

二、使用示例

...

/// 方案列表
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;
  }
}

三、源码

//
//  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 的索引和模型透传到外部,子项随意定制;
3、如果想进一步提高开发效率需要自己开发批量 API 文件生成工具和 json 转 model 工具配合使用。至此,列表开发效率已经到了一个新的瓶颈:API参数还得手动配置。

初步思路:后端接口文档返回 json 固定格式,通过 json 转 API 工具自动生成API文件(应该包含 url,参数及参数类型,必填校验等等)。需要后端配合,后面再说。

github