别再手写筛选栏了:一个可组合的 Flutter 选择组件(5 入口 × 7 委托)

146 阅读9分钟

从一条筛选栏说起:需求、现有组件的缺口、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-100100-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元'),
      // ...
    };

运行效果:

goods.gif

features 对比表

下表数据截至 2026 年 9 月,likes 数与"最后发布"时间以 pub.dev 页面为准,欢迎评论区纠错。

维度multi_select_flutterflutter_multi_select_itemsdropdown_flutterfl_select
入口形态Dialog / BottomSheet / Chip仅内联容器仅下拉框inline / button / bar / dialog / bottom sheet
内容布局列表 / chipchip列表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
view.gif
PopupSelectButtonPopupSelectBar
button.gifbar.gif
showSelectshowModalBottomSelect
dialog.gifbottom_sheet.gif

第二层是委托(delegate),决定条目怎么排、数据怎么来:扁平数据用 ListSelectDelegate / GridSelectDelegate / WrapSelectDelegate,两级分类数据用 CascadingSelectDelegate / TabNavSelectDelegate / SideNavSelectDelegate / ExpandableSelectDelegate,共 7 种:

ListSelectDelegateGridSelectDelegateWrapSelectDelegate
list.jpggrid.jpgwrap.jpg
CascadingSelectDelegateTabNavSelectDelegate
cascading.jpgsidenav.jpg
SideNavSelectDelegateExpandableSelectDelegate
tabnav.jpgexpandable.jpg

两层任意组合:同一个 CascadingSelectDelegate,今天嵌在 SelectView 里,明天塞进 showModalBottomSelect,一行不用改。数据同步给(entries 直接传,首帧渲染)或异步给(entriesLoader 返回 Future<SelectEntries>,自动骨架屏)都行。

选择结果直接序列化成 URL query 参数

筛选场景的"最后一步"——把选择树变成请求参数——是内置的。每个类别以其 id 为 key、最深选中的叶子 id 为 value;"Any"叶子解析为父 id;自定义区间格式化为 min-maxtoQueryParameters() 直接给查询串;多值数组有 4 种格式(brackets / comma / indices / delimited,覆盖 OpenAPI 的常见风格),总有一款匹配你的后端。

搜索过滤:任意 delegate 一行开启

任意 delegate 传 searchEnabled: true 即可。默认 300ms 防抖,匹配 name(大小写不敏感);提供自定义 searchPredicate 可以改按 idextra 或任意字段匹配。搜索过程中布局与已选状态不丢,取消搜索即恢复原始条目。

每一个像素都能换:itemBuilder / 骨架屏 / 错误态 / 操作栏

扁平 delegates 接受 itemBuilder,整块替换条目 widget(onTap 接回库内的选中逻辑即可);异步加载的骨架屏、加载失败的错误态、多选模式的操作栏(Apply / Reset),也都提供了对应的自定义插槽。

主题与 10 语言 i18n 内置

样式支持两个层级:单实例——delegate 上直接挂 selectedColor / onSelectedColor / gridTileTheme 等字段;全局——注册 ThemeExtensionPopupSelectBarTheme / 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_genuiGenUI 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…
在线 Playgroundflselect.zeaon.dev
Agent Skilldart run skills@ get
Issue / Featuregithub.com/amlzq/fl_se…