Flutter组件封装:网络数据选择器 - NPickRequestListBox

一、需求来源

遇到通过底部弹窗选择网络数据的需求,每次从头实现费时费力还不好维护,就想实现一种适用于任意接口的底部弹窗数据选择器。最终实现为 NPickRequestListBox 组件;将模型抽象为 泛型,通过子类化具体模型和网络请求实现具体场景。同时兼顾了代码量少、高复用、高维护的原则,完美实现最初目标。

二、使用示例

dart 复制代码
Future onPickUser() {
  final items = <UserModel>[];
  FocusManager.instance.primaryFocus?.unfocus();
  return GetBottomSheet.showCustom(
    addUnconstrainedBox: false,
    hideDragIndicator: false,
    enableDrag: true,
    child: Container(
      height: 475,
      child: NPickUsersBox(
        title: "人员选择",
        items: items ?? [],
        onChanged: (val) {
            DLog.d(
    "list: ${list.map((e) => e.toJson().filter((k, v) => v != null))}");
        },
      ),
    ),
  );
}

// [log] DLog 2024-10-18 21:48:26.797867 list: ({id: 5977258524, name: 君不强, desc: 教师, isSelected: false})

三、源码

1、NPickRequestListBox 媒体信息展示
php 复制代码
/// 基于接口的搜索选择列表(子类实现 NPickUsersBox)
class NPickRequestListBox<E> extends StatefulWidget {
  const NPickRequestListBox({
    super.key,
    this.title = "请选择",
    this.leading,
    this.placeholder = "搜索",
    required this.items,
    required this.onChanged,
    required this.requestList,
    this.onSelectedTap,
    required this.cbName,
    required this.selected,
    this.nameWidget,
    this.itemBuilder,
  });

  /// 标题
  final String title;

  /// leading 自定义
  final Widget? leading;

  /// 提示语
  final String placeholder;

  /// 默认值
  final List<E> items;

  /// 回调
  final ValueChanged<List<E>> onChanged;

  /// 请求列表
  final Future<List<E>> Function(
      bool isRefresh, int pageNo, int pageSize, String? search) requestList;

  /// 选择
  final E Function(E e)? onSelectedTap;

  /// E 转 name
  final String Function(E e) cbName;

  final Widget Function(int index, E e)? nameWidget;

  /// 相等对比
  final bool Function(List<E> items, E? b) selected;

  /// 子项卡片自定义
  final Widget Function({int index, E e})? itemBuilder;

  @override
  State<NPickRequestListBox<E>> createState() => _NPickRequestListBoxState<E>();
}

class _NPickRequestListBoxState<E> extends State<NPickRequestListBox<E>> {
  final refreshViewController = NRefreshViewController<E>();

  var search = "";

  E? selecetdModel;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomInset: false,
      backgroundColor: Colors.white,
      appBar: NAppBar(
        backgroundColor: Colors.white,
        titleStr: widget.title,
        leadingWidth: 70,
        leading: widget.leading ??
            buildLeading(
              title: "取消",
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
      ),
      body: Column(
        // mainAxisSize: MainAxisSize.min,
        children: [
          Padding(
            padding: const EdgeInsets.only(
              left: 16,
              right: 16,
              bottom: 12,
            ),
            child: NSearchBar(
              placeholder: widget.placeholder,
              backgroundColor: bgColorF3F3F3,
              onChanged: (val) {
                search = val;
                refreshViewController.onRefresh();
              },
              // onCancel: () {
              //   Get.back();
              // },
            ),
          ),
          Expanded(
            child: buildBody(),
          ),
        ],
      ),
    );
  }

  Widget buildLeading({
    required String title,
    required VoidCallback onPressed,
  }) {
    return GestureDetector(
      onTap: onPressed,
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12),
        decoration: BoxDecoration(
            // color: Colors.green,
            // border: Border.all(color: Colors.blue),
            ),
        child: const Text(
          "取消",
          style: TextStyle(
            fontWeight: FontWeight.w500,
            fontSize: 16.0,
            color: fontColor737373,
          ),
        ),
      ),
    );
  }

  Widget buildBody() {
    return buildSubPage();
  }

  Widget buildSubPage() {
    final primary = context.primaryColor;
    return NRefreshView<E>(
      controller: refreshViewController,
      pageSize: 30,
      onRequest: (bool isRefresh, int page, int pageSize, last) async {
        return await widget.requestList(isRefresh, page, pageSize, search);
      },
      itemBuilder: (BuildContext context, int index, model) {
        final isSelected = widget.selected(widget.items, model);
        final textColor = isSelected ? primary : fontColor;
        final color = isSelected ? primary : Colors.transparent;

        final name = widget.cbName(model) ?? "--";

        void onTapItem() {
          // YLog.d("${jsonEncode(model.toJson())}");
          selecetdModel = model;
          final modelNew = widget.onSelectedTap?.call(model) ?? model;
          widget.items.add(modelNew);
          widget.onChanged(widget.items);
          Navigator.of(context).pop();
        }

        if (widget.itemBuilder != null) {
          return GestureDetector(
            onTap: onTapItem,
            child: widget.itemBuilder?.call(index: index, e: model),
          );
        }

        return Column(
          children: [
            ListTile(
              dense: true,
              onTap: onTapItem,
              title: widget.nameWidget?.call(index, model) ??
                  NText(
                    name ?? "",
                    fontSize: 16,
                    color: textColor,
                  ),
              trailing: Icon(Icons.check, color: color),
            ),
            const Divider(indent: 15, endIndent: 15),
          ],
        );
      },
    );
  }
}
2、NPickUsersBox 组件源码
dart 复制代码
/// 人员选择盒子
class NPickUsersBox extends NPickRequestListBox<UserModel> {
  NPickUsersBox({
    super.key,
    super.title,
    required super.items,
    required super.onChanged,
  }) : super(
          leading: const SizedBox(),
          requestList: (isRefresh, pageNo, pageSize, search) async {
            // var api = UserListApi(
            //   pageNo: pageNo,
            //   pageSize: pageSize,
            //   name: search,
            // );
            //
            // var tuple = await api.fetchModels<UserModel>(
            //   onValue: (respone) => respone["result"]?["content"],
            //   onModel: (e) => UserModel.fromJson(e),
            // );

            // var list = tuple.result ?? <UserModel>[];
            // YLog.d("$widget requestList: ${list.length}");
            if (isRefresh) {
              return List.generate(pageSize, (index) {
                return UserModel(
                  id: "${1000 + index}",
                  name: 3.generateChars(chars: "张王李赵一二三四"),
                  desc: ["教师", "公务员", "个体户", "消防员"].randomOne,
                );
              });
            }

            var list = List.generate(pageSize, (index) {
              return UserModel(
                id: 10.generateChars(chars: "123456789"),
                name: 3.generateChars(chars: "君子自强不息"),
                desc: [
                  "教师",
                  "公务员",
                  "个体户",
                  "消防员",
                ].randomOne,
              );
            });
            return list;
          },
          onSelectedTap: (e) {
            return e;
          },
          cbName: (e) {
            final realName = e.name ?? "-";
            return realName;
          },
          selected: (items, b) {
            final result = items.map((e) => e.id).contains(b?.id);
            return result;
          },
          nameWidget: (index, model) {
            final name = model.name ?? "-";

            var desc = model.desc ?? "";
            if (desc.isNotEmpty) {
              desc = "($desc)";
            }

            return Row(
              children: [
                Flexible(
                  child: NText(
                    name,
                    fontSize: 16,
                    color: fontColor,
                  ),
                ),
                NText(
                  desc,
                  fontSize: 16,
                  color: fontColor999999,
                ),
              ],
            );
          },
        );
}

最后、总结

  • 此组件基于核心组件 - NRefreshView,不了解的可以翻看之前的文章。

  • NPickRequestListBox组件暴露出了很多封装好的属性,可以通过这些属性定制子项卡片显示,记忆之前选择态,多选,单选等等。

  • 这个组件适合对数据即时性较高的场景,如果即时性不高通过普通的弹窗即可。

github:NPickUsersBox

github:NPickRequestListBox

相关推荐
再学一点就睡7 分钟前
Cookie、LocalStorage 和 SessionStorage 的全面解析
前端
余人于RenYu17 分钟前
前端插件使用汇总
前端·javascript
马拉萨的春天26 分钟前
flutter 项目结构目录以及pubspec.ymal等文件描述
flutter
2301_7891695430 分钟前
前端对接下载文件接口、对接dart app
前端
邴越39 分钟前
OpenAI Function Calling 函数调用能力与外部交互
开发语言·前端·javascript
uhakadotcom1 小时前
React 和 Next.js 的基础知识对比
前端·面试·github
Billy Qin1 小时前
Tree - Shaking
前端·javascript·vue.js
Theodore_10221 小时前
ES6(8) Fetch API 详解
开发语言·前端·javascript·ecmascript·es6
月明长歌1 小时前
Vue + Axios + Mock.js 全链路实操:从封装到数据模拟的深度解析
前端·javascript·vue.js·elementui·es6
CodeCraft Studio1 小时前
Excel处理控件Spire.XLS系列教程:C# 合并、或取消合并 Excel 单元格
前端·c#·excel