一、需求来源
遇到通过底部弹窗选择网络数据的需求,每次从头实现费时费力还不好维护,就想实现一种适用于任意接口的底部弹窗数据选择器。最终实现为 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组件暴露出了很多封装好的属性,可以通过这些属性定制子项卡片显示,记忆之前选择态,多选,单选等等。
-
这个组件适合对数据即时性较高的场景,如果即时性不高通过普通的弹窗即可。