在 Flutter 开发中,下拉选择器是表单填写、条件筛选、数据选择等场景的高频组件。原生DropdownButton仅支持基础单选,多选和搜索筛选需手动实现,存在样式定制难、交互体验差、适配场景有限等问题。
本文封装的CommonDropdown 通用下拉选择器,整合「单选 / 多选切换、内置搜索筛选、全样式自定义、轻量无依赖」四大核心能力,适配表单、筛选等高频业务场景,一行代码即可集成,兼顾易用性与灵活性。
一、核心优势(精准解决开发痛点)
| 核心能力 | 解决痛点 | 核心价值 |
|---|---|---|
| 🎯 单选 / 多选一键切换 | 原生仅支持单选,多选需手动封装 | 通过isMultiple参数一键切换,适配表单单选、筛选多选等不同场景 |
| 🔍 内置搜索筛选 | 选项过多时查找困难 | 支持关键词模糊搜索(不区分大小写),快速定位目标选项 |
| 🎨 全维度样式自定义 | 原生样式难以适配产品视觉规范 | 支持自定义高度、背景色、圆角、边框,贴合 APP 设计风格 |
| 🚀 轻量无依赖 | 第三方组件体积大、依赖复杂 | 仅依赖 Flutter 核心库,无额外依赖,打包体积小、性能优 |
| 📋 表单友好适配 | 选中文本溢出、提示文案不统一 | 自动处理选中文本省略、空值提示,适配表单填写场景 |
| ✨ 交互体验优化 | 搜索框无清空按钮、下拉菜单高度失控 | 搜索框带清除按钮、下拉列表限制最大高度,避免超出屏幕 |
| 🌙 深色模式适配 | 深色模式下样式冲突,适配繁琐 | 一键开启深色模式适配,自动切换文本 / 背景 / 边框色 |
| ⏳ 异步加载支持 | 异步选项加载无状态提示 | 内置加载 / 空状态,适配接口异步获取选项场景 |
二、核心配置速览(关键参数一目了然)
| 配置参数 | 类型 | 默认值 | 核心作用 | 适用场景 |
|---|---|---|---|---|
| options | List<String> | -(必传) | 下拉选项列表(不可为空) | 所有场景 |
| value | dynamic | null | 当前选中值(单选:String;多选:List<String>) | 所有场景 |
| onChanged | ValueChanged<dynamic> | -(必传) | 选择回调(返回选中值,类型与isMultiple匹配) |
所有场景 |
| isMultiple | bool | false | 是否开启多选模式 | 表单(单选)/ 筛选(多选) |
| enableSearch | bool | true | 是否启用搜索筛选功能 | 选项 > 5 条时建议开启 |
| hintText | String | "请选择" | 未选择时的提示文本 | 表单填写场景 |
| height | double | 44 | 选择器主体高度 | 适配不同布局高度需求 |
| bgColor | Color | Colors.white | 选择器背景色 | 全局样式统一 |
| borderRadius | double | 8 | 选择器 / 下拉菜单圆角半径 | 视觉风格定制 |
| borderColor | Color | Color(0xFFE5E5E5) | 边框颜色 | 全局样式统一 |
| textStyle | TextStyle | 16 号黑色 | 选中 / 选项文本样式 | 字体 / 颜色定制 |
| hintStyle | TextStyle | 16 号灰色 | 提示文本样式 | 表单提示风格 |
| adaptDarkMode | bool | false | 是否适配深色模式 | 支持深色模式的 APP |
| darkBgColor | Color | Color(0xFF2C2C2C) | 深色模式背景色 | 深色模式适配 |
| darkBorderColor | Color | Color(0xFF444444) | 深色模式边框色 | 深色模式适配 |
| isLoading | bool | false | 是否显示加载状态 | 异步加载选项场景 |
三、生产级完整代码(可直接复制)
dart
import 'package:flutter/material.dart';
/// 通用下拉选择器(支持单选/多选、搜索筛选、全样式自定义、深色模式适配)
class CommonDropdown extends StatefulWidget {
// 必选核心参数
final List<String> options; // 选项列表(不可为空)
final ValueChanged<dynamic> onChanged; // 选择回调
// 选中值(单选:String;多选:List<String>)
final dynamic value;
// 功能配置
final bool isMultiple; // 是否多选
final bool enableSearch; // 是否启用搜索
final String hintText; // 未选择提示文本
final bool adaptDarkMode; // 是否适配深色模式
final bool isLoading; // 是否加载中(异步选项场景)
// 样式配置
final double height; // 选择器高度
final Color bgColor; // 背景色
final double borderRadius; // 圆角半径
final Color borderColor; // 边框颜色
final TextStyle textStyle; // 文本样式
final TextStyle hintStyle; // 提示文本样式
// 深色模式样式配置
final Color darkBgColor; // 深色模式背景色
final Color darkBorderColor; // 深色模式边框色
final TextStyle darkTextStyle; // 深色模式文本样式
final TextStyle darkHintStyle; // 深色模式提示文本样式
const CommonDropdown({
super.key,
required this.options,
required this.onChanged,
this.value,
this.isMultiple = false,
this.enableSearch = true,
this.hintText = "请选择",
this.height = 44,
this.bgColor = Colors.white,
this.borderRadius = 8,
this.borderColor = const Color(0xFFE5E5E5),
this.textStyle = const TextStyle(fontSize: 16, color: Color(0xFF333333)),
this.hintStyle = const TextStyle(fontSize: 16, color: Color(0xFF999999)),
this.adaptDarkMode = false,
this.darkBgColor = const Color(0xFF2C2C2C),
this.darkBorderColor = const Color(0xFF444444),
this.darkTextStyle = const TextStyle(fontSize: 16, color: Color(0xFFE5E5E5)),
this.darkHintStyle = const TextStyle(fontSize: 16, color: Color(0xFF777777)),
this.isLoading = false,
}) : assert(options.isNotEmpty || isLoading, "【CommonDropdown】选项列表不可为空(加载状态除外)!");
@override
State<CommonDropdown> createState() => _CommonDropdownState();
}
class _CommonDropdownState extends State<CommonDropdown> {
// 搜索控制器(带防抖)
final TextEditingController _searchController = TextEditingController();
// 筛选后的选项列表
late List<String> _filteredOptions;
// 焦点节点(控制搜索框键盘)
final FocusNode _searchFocusNode = FocusNode();
// 防抖定时器
Timer? _debounceTimer;
@override
void initState() {
super.initState();
// 初始化筛选列表为原始选项
_filteredOptions = List.from(widget.options);
// 监听搜索框清空(优化交互)
_searchController.addListener(_onSearchTextChanged);
}
@override
void didUpdateWidget(covariant CommonDropdown oldWidget) {
super.didUpdateWidget(oldWidget);
// 原始选项变化时,重置筛选列表和搜索框
if (widget.options != oldWidget.options) {
_filteredOptions = List.from(widget.options);
_searchController.clear();
setState(() {});
}
// 选中值变化时刷新UI
if (widget.value != oldWidget.value) {
setState(() {});
}
// 加载状态变化时刷新
if (widget.isLoading != oldWidget.isLoading) {
setState(() {});
}
}
@override
void dispose() {
// 资源释放:避免内存泄漏
_searchController.dispose();
_searchFocusNode.dispose();
_debounceTimer?.cancel();
super.dispose();
}
/// 检查是否为深色模式
bool _isDarkMode() {
if (!widget.adaptDarkMode) return false;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark;
}
/// 搜索文本变化监听(清空时重置筛选)
void _onSearchTextChanged() {
if (_searchController.text.isEmpty) {
_filterOptions("");
}
}
/// 筛选选项(防抖+不区分大小写)
void _filterOptions(String keyword) {
// 防抖处理:300ms内多次输入仅执行最后一次
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
if (keyword.isEmpty) {
_filteredOptions = List.from(widget.options);
} else {
_filteredOptions = widget.options
.where((option) => option.toLowerCase().contains(keyword.toLowerCase()))
.toList();
}
if (mounted) {
setState(() {});
}
});
}
/// 构建选中文本(处理单选/多选、空值、溢出)
String _buildSelectedText() {
// 加载中显示占位
if (widget.isLoading) {
return "加载中...";
}
// 空值显示提示文本
if (widget.value == null) {
return widget.hintText;
}
// 多选模式:拼接选中项(超出1行自动省略)
if (widget.isMultiple) {
final List<String> selectedList = widget.value is List<String>
? List.from(widget.value)
: [];
if (selectedList.isEmpty) {
return widget.hintText;
}
return selectedList.join("、");
}
// 单选模式:直接显示选中值
return widget.value.toString();
}
/// 检查选项是否被选中(兼容单选/多选)
bool _isOptionSelected(String option) {
if (widget.value == null) return false;
if (widget.isMultiple) {
final List<String> selectedList = widget.value is List<String>
? List.from(widget.value)
: [];
return selectedList.contains(option);
} else {
return widget.value.toString() == option;
}
}
/// 全选/取消全选逻辑
void _toggleSelectAll() {
final List<String> allOptions = List.from(widget.options);
final List<String> currentSelected = widget.value is List<String>
? List.from(widget.value)
: [];
if (currentSelected.length == allOptions.length) {
// 取消全选
widget.onChanged([]);
} else {
// 全选
widget.onChanged(allOptions);
}
}
/// 显示下拉菜单
void _showDropdown() {
// 加载中不响应点击
if (widget.isLoading) return;
// 打开下拉前重置搜索框和筛选列表
_searchController.clear();
_filterOptions("");
showModalBottomSheet(
context: context,
isScrollControlled: false, // 避免键盘顶起布局
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(widget.borderRadius),
),
),
backgroundColor: _isDarkMode() ? widget.darkBgColor : widget.bgColor,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 搜索框(可选)
if (widget.enableSearch)
TextField(
controller: _searchController,
focusNode: _searchFocusNode,
decoration: InputDecoration(
hintText: "搜索选项",
hintStyle: _isDarkMode()
? widget.darkHintStyle.copyWith(fontSize: 14)
: widget.hintStyle.copyWith(fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: BorderSide(
color: _isDarkMode() ? widget.darkBorderColor : widget.borderColor,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: BorderSide(
color: _isDarkMode() ? widget.darkBorderColor : widget.borderColor,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
borderSide: const BorderSide(color: Color(0xFF0066FF)),
),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: _isDarkMode() ? widget.darkHintStyle.color : const Color(0xFF999999),
),
onPressed: () {
_searchController.clear();
_filterOptions("");
},
)
: Icon(
Icons.search,
color: _isDarkMode() ? widget.darkHintStyle.color : const Color(0xFF999999),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
onChanged: _filterOptions,
autofocus: true,
style: _isDarkMode()
? widget.darkTextStyle.copyWith(fontSize: 14)
: widget.textStyle.copyWith(fontSize: 14),
),
if (widget.enableSearch) const SizedBox(height: 16),
// 多选模式:全选/取消全选按钮
if (widget.isMultiple && widget.options.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: _toggleSelectAll,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(40, 24),
),
child: Text(
widget.value is List<String> && (widget.value as List<String>).length == widget.options.length
? "取消全选"
: "全选",
style: const TextStyle(
color: Color(0xFF0066FF),
fontSize: 14,
),
),
),
),
),
// 选项列表(限制最大高度,避免超出屏幕)
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 300),
child: widget.isLoading
? // 加载状态
const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: _filteredOptions.isEmpty
? // 无匹配选项提示
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Text(
"暂无匹配选项",
style: _isDarkMode()
? widget.darkHintStyle.copyWith(fontSize: 14)
: const TextStyle(color: Color(0xFF999999), fontSize: 14),
),
),
)
: // 选项列表
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(), // 避免滚动冲突
itemCount: _filteredOptions.length,
itemBuilder: (context, index) {
final option = _filteredOptions[index];
final isSelected = _isOptionSelected(option);
final currentTextStyle = _isDarkMode() ? widget.darkTextStyle : widget.textStyle;
return InkWell(
onTap: () {
// 处理选择逻辑
if (widget.isMultiple) {
final List<String> newValues = widget.value is List<String>
? List.from(widget.value)
: [];
if (newValues.contains(option)) {
newValues.remove(option);
} else {
newValues.add(option);
}
widget.onChanged(newValues);
} else {
widget.onChanged(option);
Navigator.pop(context); // 单选后直接关闭
}
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
option,
style: currentTextStyle.copyWith(
color: isSelected ? const Color(0xFF0066FF) : currentTextStyle.color,
fontSize: 15,
),
),
if (isSelected)
const Icon(
Icons.check,
color: Color(0xFF0066FF),
size: 20,
),
],
),
),
);
},
),
),
// 多选模式:底部操作按钮(确认/取消)
if (widget.isMultiple && _filteredOptions.isNotEmpty && !widget.isLoading)
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
backgroundColor: _isDarkMode() ? const Color(0xFF3A3A3A) : const Color(0xFFF5F5F5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text(
"取消",
style: TextStyle(
color: _isDarkMode() ? const Color(0xFFCCCCCC) : const Color(0xFF666666),
fontSize: 15,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
backgroundColor: const Color(0xFF0066FF),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
"确认",
style: TextStyle(color: Colors.white, fontSize: 15),
),
),
),
],
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final isDark = _isDarkMode();
final currentBgColor = isDark ? widget.darkBgColor : widget.bgColor;
final currentBorderColor = isDark ? widget.darkBorderColor : widget.borderColor;
final currentTextStyle = isDark ? widget.darkTextStyle : widget.textStyle;
final currentHintStyle = isDark ? widget.darkHintStyle : widget.hintStyle;
return InkWell(
onTap: _showDropdown,
borderRadius: BorderRadius.circular(widget.borderRadius),
child: Container(
height: widget.height,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: currentBgColor,
border: Border.all(color: currentBorderColor),
borderRadius: BorderRadius.circular(widget.borderRadius),
),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 选中文本(处理溢出)
Expanded(
child: Text(
_buildSelectedText(),
style: widget.value == null || widget.isLoading ? currentHintStyle : currentTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis, // 文本溢出时省略
),
),
// 下拉箭头
Padding(
padding: const EdgeInsets.only(left: 8),
child: Icon(
Icons.arrow_drop_down,
color: currentHintStyle.color,
size: 20,
),
),
],
),
),
);
}
}
四、五大高频场景实战示例(灵活适配不同需求)
场景 1:表单单选(性别选择,无搜索)
适用场景 :用户注册 / 信息填写表单中的性别选择,无需搜索功能实现要点:关闭搜索、单选模式、自定义提示文本、适配表单样式
dart
class FormGenderSelect extends StatefulWidget {
@override
State<FormGenderSelect> createState() => _FormGenderSelectState();
}
class _FormGenderSelectState extends State<FormGenderSelect> {
String? _selectedGender; // 单选值:String类型
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: CommonDropdown(
options: ["男", "女", "保密"],
value: _selectedGender,
onChanged: (value) => setState(() => _selectedGender = value),
isMultiple: false, // 单选模式
enableSearch: false, // 关闭搜索
hintText: "请选择性别",
borderColor: const Color(0xFFE6E6E6),
bgColor: const Color(0xFFFAFAFA),
adaptDarkMode: true, // 适配深色模式
),
);
}
}
场景 2:筛选多选(爱好选择,带搜索 + 全选)
适用场景 :个人中心 / 筛选页面的爱好选择,支持多选和搜索实现要点:开启多选、保留搜索、自定义样式、全选功能
dart
class HobbySelect extends StatefulWidget {
@override
State<HobbySelect> createState() => _HobbySelectState();
}
class _HobbySelectState extends State<HobbySelect> {
List<String> _selectedHobbies = []; // 多选值:List<String>类型
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: CommonDropdown(
options: ["读书", "运动", "旅游", "摄影", "音乐", "绘画", "美食", "游戏"],
value: _selectedHobbies,
onChanged: (value) => setState(() => _selectedHobbies = value),
isMultiple: true, // 多选模式
enableSearch: true, // 开启搜索
hintText: "请选择爱好(可多选)",
borderRadius: 12,
bgColor: Colors.white,
borderColor: const Color(0xFF0066FF).withOpacity(0.1),
adaptDarkMode: true,
),
);
}
}
场景 3:城市选择(单选 + 搜索,异步加载)
适用场景 :地址填写 / 定位页面的城市选择,选项多需搜索且异步加载实现要点:单选模式、开启搜索、异步加载状态、适配大量选项
dart
class CitySelect extends StatefulWidget {
@override
State<CitySelect> createState() => _CitySelectState();
}
class _CitySelectState extends State<CitySelect> {
String? _selectedCity;
List<String> _cityList = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
// 模拟接口请求加载城市列表
_loadCityList();
}
Future<void> _loadCityList() async {
try {
await Future.delayed(const Duration(seconds: 1)); // 模拟接口延迟
setState(() {
_cityList = [
"北京", "上海", "广州", "深圳", "杭州", "成都", "重庆", "西安",
"南京", "武汉", "长沙", "郑州", "青岛", "厦门", "宁波", "苏州",
"天津", "沈阳", "长春", "哈尔滨", "石家庄", "太原", "济南", "合肥"
];
_isLoading = false;
});
} catch (e) {
debugPrint("加载城市列表失败:$e");
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: CommonDropdown(
options: _cityList,
value: _selectedCity,
onChanged: (value) => setState(() => _selectedCity = value),
isMultiple: false,
enableSearch: true, // 开启搜索(关键)
hintText: "请选择城市",
height: 48,
textStyle: const TextStyle(fontSize: 15, color: Color(0xFF1A1A1A)),
isLoading: _isLoading, // 异步加载状态
adaptDarkMode: true,
),
);
}
}
场景 4:品类筛选(多选 + 自定义胶囊样式)
适用场景 :电商 APP 商品筛选,多选品类且自定义视觉风格实现要点:多选模式、自定义文本 / 边框样式、胶囊圆角、适配深色背景
dart
class CategoryFilter extends StatefulWidget {
@override
State<CategoryFilter> createState() => _CategoryFilterState();
}
class _CategoryFilterState extends State<CategoryFilter> {
List<String> _selectedCategories = [];
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: CommonDropdown(
options: ["数码", "服装", "食品", "家居", "美妆", "图书", "家电", "运动"],
value: _selectedCategories,
onChanged: (value) => setState(() => _selectedCategories = value),
isMultiple: true,
enableSearch: true,
hintText: "请选择品类",
bgColor: const Color(0xFFF0F7FF),
borderColor: const Color(0xFF0066FF),
textStyle: const TextStyle(color: Color(0xFF0066FF), fontSize: 16),
hintStyle: const TextStyle(color: Color(0xFF6699FF), fontSize: 16),
borderRadius: 22, // 胶囊样式
height: 44,
adaptDarkMode: true,
darkBgColor: const Color(0xFF1A2B47),
darkBorderColor: const Color(0xFF3385FF),
),
);
}
}
场景 5:表单验证(结合 Flutter Form)
适用场景 :注册 / 提交表单,需验证下拉选择器必填项实现要点 :结合FormField、验证逻辑、错误提示
dart
class RegisterForm extends StatefulWidget {
@override
State<RegisterForm> createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
String? _selectedGender;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
// 性别选择(带表单验证)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: FormField<String>(
validator: (value) => value == null ? "请选择性别" : null,
builder: (field) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CommonDropdown(
options: ["男", "女", "保密"],
value: _selectedGender,
onChanged: (value) {
setState(() => _selectedGender = value);
field.didChange(value);
},
isMultiple: false,
enableSearch: false,
hintText: "请选择性别",
adaptDarkMode: true,
),
if (field.hasError)
Padding(
padding: const EdgeInsets.only(top: 4, left: 16),
child: Text(
field.errorText!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
],
),
),
),
// 提交按钮
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// 表单验证通过,提交数据
debugPrint("性别:$_selectedGender");
}
},
child: const Text("提交"),
),
],
),
);
}
}
五、工程化最佳实践(提升项目可维护性)
1. 全局样式统一管理
定义全局下拉选择器样式常量,确保 APP 内风格一致,便于统一修改:
dart
/// 全局下拉选择器样式常量
class DropdownStyles {
// 表单默认样式(单选、无搜索、适配深色模式)
static CommonDropdown formDropdown({
required List<String> options,
required dynamic value,
required ValueChanged<dynamic> onChanged,
bool isMultiple = false,
String hintText = "请选择",
bool isLoading = false,
}) => CommonDropdown(
options: options,
value: value,
onChanged: onChanged,
isMultiple: isMultiple,
enableSearch: false,
hintText: hintText,
bgColor: const Color(0xFFFAFAFA),
borderColor: const Color(0xFFE6E6E6),
textStyle: const TextStyle(fontSize: 15, color: Color(0xFF333333)),
hintStyle: const TextStyle(fontSize: 15, color: Color(0xFF999999)),
adaptDarkMode: true,
isLoading: isLoading,
);
// 筛选页样式(多选、带搜索、胶囊圆角)
static CommonDropdown filterDropdown({
required List<String> options,
required dynamic value,
required ValueChanged<dynamic> onChanged,
String hintText = "请选择",
bool isLoading = false,
}) => CommonDropdown(
options: options,
value: value,
onChanged: onChanged,
isMultiple: true,
enableSearch: true,
hintText: hintText,
bgColor: Colors.white,
borderColor: const Color(0xFF0066FF).withOpacity(0.1),
borderRadius: 12,
adaptDarkMode: true,
isLoading: isLoading,
);
}
// 使用示例
DropdownStyles.formDropdown(
options: ["男", "女", "保密"],
value: _selectedGender,
onChanged: (value) => setState(() => _selectedGender = value),
hintText: "请选择性别",
)
2. 结合状态管理(Provider)
避免选中值多层级传递,结合 Provider 管理选择状态:
dart
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
/// 筛选状态管理Provider
class FilterProvider extends ChangeNotifier {
List<String> _selectedCategories = [];
List<String> get selectedCategories => _selectedCategories;
void setSelectedCategories(List<String> value) {
_selectedCategories = value;
notifyListeners();
}
// 清空选中项
void clearSelected() {
_selectedCategories = [];
notifyListeners();
}
}
// 使用示例
class FilterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => FilterProvider(),
child: Consumer<FilterProvider>(
builder: (context, provider, child) => Scaffold(
appBar: AppBar(title: const Text("品类筛选")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Column(
children: [
CommonDropdown(
options: ["数码", "服装", "食品", "家居"],
value: provider.selectedCategories,
onChanged: provider.setSelectedCategories,
isMultiple: true,
enableSearch: true,
hintText: "请选择品类",
adaptDarkMode: true,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: provider.clearSelected,
child: const Text("清空选择"),
),
],
),
),
),
),
);
}
}
3. 性能优化建议
- 选项数据复用:大量选项(如城市列表)建议缓存(如静态常量),避免重复创建 List;
- 搜索防抖:选项超 100 条时,组件内置 300ms 防抖,减少高频筛选(已集成);
- 避免频繁重建 :使用
const构造函数、缓存不变的选项列表; - 懒加载选项 :异步加载选项时,通过
isLoading参数显示加载状态,提升体验; - 资源释放 :确保
dispose中释放搜索控制器、焦点节点、防抖定时器(已集成); - 列表优化 :选项列表使用
shrinkWrap: true+ClampingScrollPhysics,避免滚动冲突; - 深色模式优化 :仅在需要时开启
adaptDarkMode,减少不必要的样式计算。
4. 无障碍适配
为下拉选择器添加语义标签,提升屏幕阅读器体验:
dart
// 表单必填下拉选择器
Semantics(
label: "性别选择(必填)",
hint: "请选择男、女或保密",
child: CommonDropdown(
options: ["男", "女", "保密"],
value: _selectedGender,
onChanged: (value) => setState(() => _selectedGender = value),
),
)
// 筛选多选下拉选择器
Semantics(
label: "爱好筛选(可多选)",
hint: "支持搜索,可全选或取消全选",
child: CommonDropdown(
options: ["读书", "运动", "旅游"],
value: _selectedHobbies,
onChanged: (value) => setState(() => _selectedHobbies = value),
isMultiple: true,
),
)
六、避坑指南(解决 90% 开发痛点)
| 问题场景 | 常见原因 | 解决方案 |
|---|---|---|
| 多选时 value 报错 | value 类型不是 List<String> | 1. 多选初始值设为空列表[]2. 回调中确保传入List<String>类型 |
| 搜索筛选区分大小写 | 筛选逻辑未统一转小写 | 组件内置toLowerCase()处理,无需手动修改 |
| 选项为空触发断言 | 传入空的 options 列表且非加载状态 | 1. 确保 options 非空2. 异步加载时设置isLoading: true |
| 下拉菜单高度溢出 | 未限制列表最大高度 | 组件内置ConstrainedBox(maxHeight: 300),无需手动设置 |
| 选中文本溢出显示不全 | 未设置文本省略 | 组件内置maxLines: 1+overflow: TextOverflow.ellipsis |
| 内存泄漏 | 未释放搜索控制器 / 防抖定时器 | 组件在dispose中释放资源,无需手动处理 |
| 多选后下拉菜单直接关闭 | 单选逻辑影响多选 | 多选模式下移除自动关闭,添加确认 / 取消按钮手动关闭(已集成) |
| 搜索框清空后筛选未重置 | 未监听搜索框清空事件 | 组件内置搜索框清空监听,自动重置筛选(已集成) |
| 异步加载选项无状态提示 | 未设置isLoading |
异步加载时设置isLoading: true,显示加载动画 |
| 深色模式样式冲突 | 未开启adaptDarkMode |
1. 设置adaptDarkMode: true2. 配置深色模式样式参数 |
| 搜索高频触发筛选 | 无防抖处理 | 组件内置 300ms 防抖,减少性能消耗(已集成) |
七、扩展能力(按需定制)
1. 自定义选项样式(带图标 / 颜色)
支持选项带图标、自定义选中样式:
dart
// 1. 扩展组件参数(新增图标配置)
final List<Widget>? optionIcons; // 选项图标列表(与options一一对应)
// 2. 优化选项列表构建逻辑(在itemBuilder中添加)
final icon = widget.optionIcons != null && index < widget.optionIcons!.length
? widget.optionIcons![index]
: null;
return InkWell(
onTap: () { /* 选择逻辑不变 */ },
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// 选项图标
if (icon != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: icon,
),
Expanded(
child: Text(
option,
style: currentTextStyle.copyWith(
color: isSelected ? const Color(0xFF0066FF) : currentTextStyle.color,
fontSize: 15,
),
),
),
if (isSelected)
const Icon(
Icons.check,
color: Color(0xFF0066FF),
size: 20,
),
],
),
),
);
// 3. 使用示例
CommonDropdown(
options: ["微信", "支付宝", "银行卡"],
value: _selectedPayType,
onChanged: (value) => setState(() => _selectedPayType = value),
optionIcons: const [
Icon(Icons.wechat, color: Color(0xFF07C160)),
Icon(Icons.alipay, color: Color(0xFF1677FF)),
Icon(Icons.credit_card, color: Color(0xFFFF6700)),
],
adaptDarkMode: true,
)
2. 限制多选最大数量
支持设置多选时的最大选中数,避免选择过多:
dart
// 1. 扩展组件参数
final int? maxSelectCount; // 最大选中数(null表示无限制)
// 2. 优化选择逻辑(在onTap中添加)
if (widget.isMultiple) {
final List<String> newValues = widget.value is List<String>
? List.from(widget.value)
: [];
// 限制最大选中数
if (widget.maxSelectCount != null && newValues.contains(option)) {
// 取消选择不受限制
newValues.remove(option);
} else if (widget.maxSelectCount == null || newValues.length < widget.maxSelectCount!) {
// 未达最大数时允许选择
if (!newValues.contains(option)) {
newValues.add(option);
}
} else {
// 超过最大数提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("最多只能选择${widget.maxSelectCount}个选项"),
duration: const Duration(seconds: 1),
),
);
return;
}
widget.onChanged(newValues);
}
// 3. 使用示例
CommonDropdown(
options: ["读书", "运动", "旅游", "摄影"],
value: _selectedHobbies,
onChanged: (value) => setState(() => _selectedHobbies = value),
isMultiple: true,
maxSelectCount: 3, // 最多选3个
enableSearch: true,
)
3. 自定义下拉菜单高度
支持手动调整下拉菜单的最大高度:
dart
// 1. 扩展组件参数
final double maxMenuHeight; // 下拉菜单最大高度
// 2. 替换ConstrainedBox的maxHeight
ConstrainedBox(
constraints: BoxConstraints(maxHeight: widget.maxMenuHeight),
child: /* 选项列表 */,
)
// 3. 使用示例
CommonDropdown(
options: ["选项1", "选项2", "选项3"],
value: _selectedValue,
onChanged: (value) => setState(() => _selectedValue = value),
maxMenuHeight: 400, // 自定义最大高度
)
八、总结
优化后的 CommonDropdown 组件解决了原生下拉选择器的核心痛点,支持单选 / 多选、搜索筛选、深色模式适配、异步加载,适配表单、筛选等高频业务场景。通过工程化的设计思路,补充了表单验证、状态管理、性能优化等最佳实践,可直接应用于生产环境。
该组件轻量无依赖、交互体验优秀、样式高度可定制,既能减少重复开发工作,又能保证 APP 内选择器体验的一致性,是 Flutter 项目中下拉选择场景的理想解决方案。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。