Flutter 通用下拉选择组件 CommonDropdown:单选 + 搜索 + 自定义样式

在 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),一起共建开源鸿蒙跨平台生态。

相关推荐
ZHang......3 小时前
LeetCode 1114. 按序打印
java·开发语言·算法
缺点内向3 小时前
如何在 C# 中重命名 Excel 工作表并设置标签颜色
开发语言·c#·excel
Можно3 小时前
深入理解 JavaScript 函数:分类、特性与实战应用
开发语言·javascript
淼淼7633 小时前
工厂方法模式
开发语言·c++·windows·qt·工厂方法模式
lionliu05193 小时前
JavaScript 变量声明最佳实践
前端·javascript·vue.js
Hui Baby3 小时前
全局事务入口感知子事务方法-TCC
java·开发语言·数据库
AAA阿giao3 小时前
从零开始学 React:用搭积木的方式构建你的第一个网页!
前端·javascript·学习·react.js·前端框架·vite·jsx
laozhoy13 小时前
深入理解Go语言errors.As方法:灵活的错误类型识别
开发语言·后端·golang
周杰伦_Jay3 小时前
【Go 语言】核心特性、基础语法及面试题
开发语言·后端·golang