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

相关推荐
m0_74823611几秒前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo61713 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489414 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356126 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js