概述
TL;DR :fl_select 把"选择"这类 UI 拆成入口点 × 委托 × 布局三层,任意组合;同一条筛选栏里每个 tab 可以各用一套布局与选择模式,点"应用"直接拿到能塞进 URL 的 query 参数。
源于一个非常日常的业务场景:一条筛选栏上挂着好几个 tab,每个 tab 里是不同形态 的筛选面板------品牌是多选 chip、价格是网格区间、地区是级联菜单、排序是单选列表;用户点"应用"后,还要把所有条件序列化成 ?brand=a&brand=b&price=0-100 塞进 URL 发请求。
我在 pub.dev 上找了一圈,发现要把这条筛选栏拼出来,得引入一个 chip 多选容器、自己拿 Slider/TextField 拼一个区间面板、再找一个级联组件和一个单选列表,触发按钮、弹出层、Apply/Reset 操作栏、序列化逻辑全部手写------没有一个现成组件能整段接住这个场景。
拼着拼着我意识到:问题不在某一个做得不好,而在"选择"这类 UI 一直缺少一个好用的组件。于是有了 fl_select:一个高度可组合的 Flutter 选择组件------筛选栏是它的典型用例,但远不止于此。

与 pub 上的选择组件对比,fl_select 的优势在哪?
先把需求原样摆出来:
商品列表筛选栏 四个 tab: ① 品牌 ------ 多选,chip 流式布局; ② 价格 ------ 单选,网格(
0-100、100-500......外加一个"自定义区间"输入框); ③ 地区 ------ 两级级联(省 → 市); ④ 排序 ------ 单选列表。 用户点击"应用"后,所有条件要变成 URL query 参数发请求。
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 接住需求
dart
PopupSelectBar(
isScrollable: true,
tabs: const [
PopupTab(label: '品牌'),
PopupTab(label: '价格'),
PopupTab(label: '地区'),
PopupTab(label: '排序'),
],
selectDelegates: [
// ① 品牌:chip 流式布局,多选
WrapSelectDelegate(entries: brandData, selectionMode: SelectionMode.multiple),
// ② 价格:网格,单选,含"自定义区间"输入框
GridSelectDelegate(entries: priceData, crossAxisCount: 3),
// ③ 地区:级联
CascadingSelectDelegate(entries: regionData),
// ④ 排序:列表,单选
ListSelectDelegate(entries: sortData),
],
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 的数据长这样:
dart
SelectEntries get priceData => {
SelectRangeEntry.custom(), // 用户自输 min/max
SelectTextEntry.name(id: 'a', name: '0-100'),
SelectTextEntry.name(id: 'b', name: '100-500'),
// ...
};
地区 tab 的两级数据(SelectCategoryEntry.children 会自动注入 parentId,不用手写):
dart
SelectEntries get regionData => {
SelectCategoryEntry.children(
id: 'zhejiang',
name: '浙江',
children: {
SelectTextEntry.name(id: 'hangzhou', name: '杭州'),
SelectTextEntry.name(id: 'ningbo', name: '宁波'),
// ...
},
),
// ...
};
运行效果:

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 |
| 类别内布局 | --- | --- | --- | list / grid / wrap / range slider / counter |
| 选择模式 | 仅多选 | 仅多选 | 单选 + 多选 | 单选 + 多选(同一筛选栏各 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 四种按钮变体 |
showSelect |
模态对话框,await 返回结果 |
showModalBottomSelect |
模态底部弹层,同样 await 返回 |
SelectView |
PopupSelectButton |
|---|---|
![]() |
![]() |
showSelect |
showModalBottomSelect |
|---|---|
![]() |
![]() |
第二层是委托(delegate) ,决定条目怎么排、数据怎么来 :扁平数据用 ListSelectDelegate / GridSelectDelegate / WrapSelectDelegate,两级分类数据用 CascadingSelectDelegate / TabNavSelectDelegate / SideNavSelectDelegate / ExpandableSelectDelegate,共 7 种:
ListSelectDelegate |
GridSelectDelegate |
WrapSelectDelegate |
|---|---|---|
![]() |
![]() |
![]() |
CascadingSelectDelegate |
TabNavSelectDelegate |
|---|---|
![]() |
![]() |
SideNavSelectDelegate |
ExpandableSelectDelegate |
|---|---|
![]() |
![]() |
第三层是布局(layout) :分类 delegate 里,每个类别还能各选一种类别布局------同一个面板里,"价格"用 range slider、"数量"用 counter(步进器)、"标签"用 wrap chips,互不干扰。
三层任意组合 :同一个 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 用对
仓库内置了一个 Agent Skill,一条命令安装:
bash
npx skills add amlzq/fl_select
装好后,AI 编码代理会获得一套按需加载的 API 参考------生成 fl_select 代码时不再幻觉参数。
配套二: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 下一轮使用。
最后
如果你正在做列表筛选、多级分类选择,或者任何"选择类" 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 | npx skills add amlzq/fl_select |
| Issue / Feature | github.com/amlzq/fl_se... |










