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

概述

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-100100-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-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 用对

仓库内置了一个 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_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...
在线 Playground flselect.zeaon.dev
Agent Skill npx skills add amlzq/fl_select
Issue / Feature github.com/amlzq/fl_se...
相关推荐
Michaelwubo1 小时前
mysql8 主从 HA切换
android·adb
事圆则缓1 小时前
Flutter 快速上手
flutter
墨狂之逸才1 小时前
Android 工业手持机蓝牙扫描失败:为什么同时需要蓝牙和定位权限
android
Android打工仔2 小时前
Kotlin 协程源码解析(七)`delay()` 到底是如何在 `Dispatchers.Main` 上恢复的?
android·kotlin
律宏阔2 小时前
Android 红外功能实现与红外协议开发笔记:ConsumerIrManager、NEC、PWM、曼彻斯特编码
android
mmsx3 小时前
MapLibre 统一图层模型:sealed class + 声明式图层 + XML 持久化
android·源码·地图·maplibre
YUJIANYUE3 小时前
查立得万用查分安卓版(web环境+查询系统免安装单文件一键运行包)
android·前端
天空之城--4 小时前
Android Launcher 性能优化完全指南:从启动到滑动,构建极致流畅的桌面体验
android·性能优化
hunterandroid4 小时前
Android ANR 排查实战:从线上告警到主线程卡点定位
android·前端