从一条筛选栏说起:需求、现有组件的缺口、fl_select 的解法
TL;DR:fl_select 把"选择"这类 UI 拆成入口点 × 委托两层,任意组合;同一条筛选栏里每个 tab 可以各用一套布局与选择模式,点"应用"直接拿到能塞进 URL 的 query 参数。
需求场景:一条"混合形态"的筛选栏
源于一个非常日常的业务场景:一条筛选栏上挂着好几个 tab,每个 tab 里是不同形态的筛选面板——排序是单选列表、分类是级联菜单、价格是网格区间、筛选是多选 chip;用户点"应用"后,还要把所有条件序列化成 ?sort=a&cate=b&price=100-500&more=d 塞进 URL 发请求。
把需求原样摆出来:
需求:商品列表筛选栏。四个 tab: ① 综合排序 —— 单选列表; ② 分类 —— 两级级联(大类 → 子类); ③ 价格 —— 单选,网格(
0-100、100-500……外加一个"自定义区间"输入框); ④ 排序 —— 多选,chip 流式布局。 用户点击"应用"后,所有条件要变成 URL query 参数发请求。
现有组件为什么拼不出来
我在 pub.dev 上找了一圈,发现要把这条筛选栏拼出来,得引入一个 chip 多选容器、自己拿 Slider/TextField 拼一个区间面板、再找一个级联组件和一个单选列表,触发按钮、弹出层、Apply/Reset 操作栏、序列化逻辑全部手写——没有一个现成组件能整段接住这个场景。
pub.dev 上有代表性的选择组件——老牌的 multi_select_flutter(800+ likes)、颜值在线的 flutter_multi_select_items、新派下拉框 dropdown_flutter——各自的实现都不差,但都很难拼出这条筛选栏,缺口主要在这几处:
- 选择形态单一:有的只做多选,有的只做下拉列表。而筛选栏里"排序单选、类别级联、价格单选区间、排序多选"是混合形态,至少要引入三四个组件分别实现,每个都有自己的数据流和 API;
- 没有"区间"这种条目:价格的预设区间 + 自定义 min/max 输入框,只能自己拿
Slider/TextField拼面板,渲染、校验、序列化全要手写; - "在哪里弹出"和"内容长什么样"焊死:想从弹窗换成底部弹层或内联,就得换一个组件、重学一套 API、重写一遍数据流;面板布局也往往是固定的列表形态,没有 chip 流、网格、级联这些筛选栏刚需;
- 筛选语义缺失:没有"Any"条目、没有 Apply/Reset 操作栏、没有"把结果序列化成 URL query"的能力,而这些是列表筛选页每天都在做的事;
- 数据只能写死在构造参数里:筛选数据通常来自接口,异步加载、骨架屏、错误态都得自己搭;
- 不少组件已一两年未更新。
fl_select 的解法:一个 PopupSelectBar 接住需求
拼着拼着我意识到:问题不在某一个做得不好,而在"选择"这类 UI 一直缺少一个好用的组件。于是有了 fl_select:一个高度可组合的 Flutter 选择组件——筛选栏是它的典型用例,但远不止于此。
(先交代立场:fl_select 是我自己写的库,所以下文我尽量只讲它解决了什么、以及它做不了什么,好不好用留给你判断。)
上面那条需求,一个组件就能接住:
PopupSelectBar(
isScrollable: true,
tabs: const [
PopupTab(label: '综合排序'),
PopupTab(label: '分类'),
PopupTab(label: '价格'),
PopupTab(label: '筛选'),
],
selectDelegates: [
// ① 综合排序:列表,单选
ListSelectDelegate(entries: sortData),
// ② 分类:级联
CascadingSelectDelegate(entries: cateData),
// ③ 价格:网格,单选,含"自定义区间"输入框
GridSelectDelegate(entries: priceData, crossAxisCount: 3),
// ④ 筛选:chip 流式布局,多选
WrapSelectDelegate(entries: moreData, selectionMode: SelectionMode.multiple),
],
onApplied: (tabData, selected) {
// `toQueryMap()` 返回 `Map<String, List<String>>`
// `toQueryParameters()` 直接给查询串
print('toQueryMap: ${selected.toQueryMap()}');
},
);
注意三件事:
- 换布局 = 换一个 delegate,入口点(这里是筛选栏)完全不用动;
- "价格"tab 里的自定义区间只是一个条目:
SelectRangeEntry.custom(),渲染、校验、序列化(自动格式化成min-max)全部内置; onApplied拿到的就是Map<String, List<String>>,直接拼进请求。
分类 tab 的两级数据(SelectCategoryEntry.children 会自动注入 parentId,不用手写):
SelectEntries get cateData => {
SelectCategoryEntry.children(
id: 'c1',
name: '手机数码',
children: {
SelectTextEntry.name(id: 'c101', name: '智能穿戴'),
SelectTextEntry.name(id: 'c102', name: '电脑办公'),
// ...
},
),
// ...
};
价格 tab 的数据长这样:
SelectEntries get priceData => {
SelectRangeEntry.custom(), // 用户自输 min/max
SelectTextEntry.name(id: 'p1', name: '0-50元'),
SelectTextEntry.name(id: 'p2', name: '50-150元'),
// ...
};
运行效果:
features 对比表
下表数据截至 2026 年 9 月,likes 数与"最后发布"时间以 pub.dev 页面为准,欢迎评论区纠错。
| 维度 | multi_select_flutter | flutter_multi_select_items | dropdown_flutter | fl_select |
|---|---|---|---|---|
| 入口形态 | Dialog / BottomSheet / Chip | 仅内联容器 | 仅下拉框 | inline / button / bar / dialog / bottom sheet |
| 内容布局 | 列表 / chip | chip | 列表 | list / grid / wrap / cascading / tab-nav / side-nav / expandable |
| 选择模式 | 仅多选 | 仅多选 | 单选 + 多选 | 单选 + 多选(同一筛选栏各 tab 可混用) |
| 异步数据加载 | ❌ | ❌ | ✅(API 搜索) | ✅ |
| 骨架屏 / 错误态 | ❌ | ❌ | —(仅搜索 loading) | ✅ |
| 搜索过滤 | ✅(Dialog / BottomSheet) | ❌ | ✅ | ✅ |
| 区间 / 计数条目 | ❌ | ❌ | ❌ | ✅(range slider / counter / 自定义输入) |
| "Any"(清空)条目 | ❌ | ❌ | ❌ | ✅ |
| 多选操作栏(Apply / Reset) | 仅 Dialog 确认键 | ❌ | ❌ | ✅(插槽可定制) |
| 结果 → URL query | ❌ | ❌ | ❌ | ✅ |
| 表单验证 | ✅(FormField) | ❌ | ✅ | — |
| 键盘导航 | — | — | ✅ | — |
| i18n | ❌ | ❌ | ❌ | ✅(10 语言内置) |
| AI-ready(A2UI / GenUI) | ❌ | ❌ | ❌ | ✅ |
| 维护状态(截至 2026-09) | 约 3 年未更新 | 约 2 年未更新 | 活跃 | 活跃 |
公平地说,差异更多源于定位:它们是"某一种选择交互"的组件,各自擅长各自的场景;而 fl_select 想让"选择这一类 UI"全场景适用,筛选栏只是最典型的用例。
fl_select 还有其他什么特点?
作为通用选择组件,fl_select 的能力远不止一条筛选栏。这一节只过特性,代码示例、完整的 API 和用法可以直接看仓库 README,文末的在线 Playground 可以体验。
入口点 × 委托,两层正交
上一节的 PopupSelectBar 只是 5 种入口点之一。剩下的四种入口点,决定选择 UI 出现在哪里:
| 入口点 | 形态 |
|---|---|
SelectView | 内联嵌进页面、表单或对话框 body |
PopupSelectButton | 点击弹出浮层,像 PopupMenuButton,支持 text / elevated / filled / outlined 四种按钮变体 |
PopupSelectBar | 一个标签栏(PreferredSizeWidget),点击标签时会打开一个浮层选择器 |
showSelect | 模态对话框,await 返回结果 |
showModalBottomSelect | 模态底部弹层,同样 await 返回 |
SelectView |
|---|
PopupSelectButton | PopupSelectBar |
|---|---|
showSelect | showModalBottomSelect |
|---|---|
第二层是委托(delegate),决定条目怎么排、数据怎么来:扁平数据用 ListSelectDelegate / GridSelectDelegate / WrapSelectDelegate,两级分类数据用 CascadingSelectDelegate / TabNavSelectDelegate / SideNavSelectDelegate / ExpandableSelectDelegate,共 7 种:
ListSelectDelegate | GridSelectDelegate | WrapSelectDelegate |
|---|---|---|
CascadingSelectDelegate | TabNavSelectDelegate |
|---|---|
SideNavSelectDelegate | ExpandableSelectDelegate |
|---|---|
两层任意组合:同一个 CascadingSelectDelegate,今天嵌在 SelectView 里,明天塞进 showModalBottomSelect,一行不用改。数据同步给(entries 直接传,首帧渲染)或异步给(entriesLoader 返回 Future<SelectEntries>,自动骨架屏)都行。
选择结果直接序列化成 URL query 参数
筛选场景的"最后一步"——把选择树变成请求参数——是内置的。每个类别以其 id 为 key、最深选中的叶子 id 为 value;"Any"叶子解析为父 id;自定义区间格式化为 min-max。toQueryParameters() 直接给查询串;多值数组有 4 种格式(brackets / comma / indices / delimited,覆盖 OpenAPI 的常见风格),总有一款匹配你的后端。
搜索过滤:任意 delegate 一行开启
任意 delegate 传 searchEnabled: true 即可。默认 300ms 防抖,匹配 name(大小写不敏感);提供自定义 searchPredicate 可以改按 id、extra 或任意字段匹配。搜索过程中布局与已选状态不丢,取消搜索即恢复原始条目。
每一个像素都能换:itemBuilder / 骨架屏 / 错误态 / 操作栏
扁平 delegates 接受 itemBuilder,整块替换条目 widget(onTap 接回库内的选中逻辑即可);异步加载的骨架屏、加载失败的错误态、多选模式的操作栏(Apply / Reset),也都提供了对应的自定义插槽。
主题与 10 语言 i18n 内置
样式支持两个层级:单实例——delegate 上直接挂 selectedColor / onSelectedColor / gridTileTheme 等字段;全局——注册 ThemeExtension(PopupSelectBarTheme / PopupSelectButtonTheme / SelectThemeData),让每个入口自动跟随应用的明暗主题。i18n 只需加一个 SelectLocalizationsDelegate,内置 de / en / es / fr / id / ja / ko / pt / vi / zh(简繁),自动本地化 "Apply" / "Reset" / "Multiple" 文案。
最好的文档是动手玩——欢迎来在线体验 Playground。
在 AI 编码时代,fl_select 的两大配套
2026 年了,我们写 UI 的方式正在变:一半代码由 AI 编码代理生成。这对组件库提出了两个新要求:AI 得用得对(不幻觉参数),以及更进一步——AI 能不能把组件当"运行时"直接驱动?fl_select 对两个问题都有答案。
配套一:Agent Skill,让 AI 把 API 用对
fl_select 随包附带了一个 Agent Skill(Dart Skills CLI 用),位于 skills/fl_select-code-generation,专门给 AI 编码代理(Claude Code、Cursor、Codex、Windsurf、GitHub Copilot 等)参考——生成 fl_select 代码时 API 准确、不幻觉参数。
只要项目依赖了 fl_select,这个 skill 就会被自动发现,跑一条命令拉取即可:
dart run skills@ get
之后 AI 代理就有了这套按需加载的 API 参考。
配套二:fl_select_genui,让 AI Agent 直接渲染选择 UI
(补一句背景:GenUI SDK 是 Flutter 生态里的生成式 UI 方案,A2UI 是它使用的 Agent-to-UI 协议——Agent 不输出代码,只输出一段描述 UI 的 JSON,端上按 JSON 渲染成真实组件。)
更进一步。fl_select_genui是 GenUI SDK(A2UI)的桥接包:把 fl_select 的选择面板注册为聊天界面里的 CatalogItem,对话式 AI Agent 输出一段 JSON,端上就能渲染出真实、可交互的 fl_select 面板;用户选完,结果以结构化数据写回,供 agent 下一轮使用。
graph LR
A[AI Agent] -- JSON payload --> B[Select / fl_select UI]
B -- selection --> C[Map>]
最后
如果你正在做列表筛选、多级分类选择,或者任何"选择类" UI,欢迎 flutter pub add fl_select 直接上手;想先直观感受,可以去在线 Playground 把玩所有入口点、delegates、布局和行为参数,所见即所得。
如果这个思路对你有用,欢迎转给还在跟下拉框搏斗的同事;用出了 bug、缺了特性,或者想聊聊设计,issue tracker 随时敞开。觉得顺手的话,在 pub.dev 点个 like 或给仓库加个 Star 就是对我最大的支持。
链接汇总
| 资源 | 地址 |
|---|---|
| pub.dev(fl_select) | pub.dev/packages/fl… |
| pub.dev(fl_select_genui) | pub.dev/packages/fl… |
| GitHub 仓库 | github.com/amlzq/fl_se… |
| 在线 Playground | flselect.zeaon.dev |
| Agent Skill | dart run skills@ get |
| Issue / Feature | github.com/amlzq/fl_se… |