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

在 Flutter 开发中,下拉选择器是表单填写、条件筛选、数据选择等场景的高频组件。原生DropdownButton仅支持基础单选,多选和搜索筛选需手动实现,存在样式定制难、交互体验差、适配场景有限等问题。

本文封装的CommonDropdown 通用下拉选择器,整合「单选 / 多选切换、内置搜索筛选、全样式自定义、轻量无依赖」四大核心能力,适配表单、筛选等高频业务场景,一行代码即可集成,兼顾易用性与灵活性。

一、核心优势(精准解决开发痛点)

核心能力解决痛点核心价值
单选 / 多选一键切换原生仅支持单选,多选需手动封装通过isMultiple参数一键切换,适配表单单选、筛选多选等不同场景
内置搜索筛选选项过多时查找困难支持关键词模糊搜索(不区分大小写),快速定位目标选项
全维度样式自定义原生样式难以适配产品视觉规范支持自定义高度、背景色、圆角、边框,贴合 APP 设计风格
轻量无依赖第三方组件体积大、依赖复杂仅依赖 Flutter 核心库,无额外依赖,打包体积小、性能优
表单友好适配选中文本溢出、提示文案不统一自动处理选中文本省略、空值提示,适配表单填写场景
✨ 交互体验优化搜索框无清空按钮、下拉菜单高度失控搜索框带清除按钮、下拉列表限制最大高度,避免超出屏幕
深色模式适配深色模式下样式冲突,适配繁琐一键开启深色模式适配,自动切换文本 / 背景 / 边框色
⏳ 异步加载支持异步选项加载无状态提示内置加载 / 空状态,适配接口异步获取选项场景

二、核心配置速览(关键参数一目了然)

配置参数类型默认值核心作用适用场景
optionsList<String>-(必传)下拉选项列表(不可为空)所有场景
valuedynamicnull当前选中值(单选:String;多选:List<String>)所有场景
onChangedValueChanged<dynamic>-(必传)选择回调(返回选中值,类型与isMultiple匹配)所有场景
isMultipleboolfalse是否开启多选模式表单(单选)/ 筛选(多选)
enableSearchbooltrue是否启用搜索筛选功能选项 > 5 条时建议开启
hintTextString"请选择"未选择时的提示文本表单填写场景
heightdouble44选择器主体高度适配不同布局高度需求
bgColorColorColors.white选择器背景色全局样式统一
borderRadiusdouble8选择器 / 下拉菜单圆角半径视觉风格定制
borderColorColorColor(0xFFE5E5E5)边框颜色全局样式统一
textStyleTextStyle16 号黑色选中 / 选项文本样式字体 / 颜色定制
hintStyleTextStyle16 号灰色提示文本样式表单提示风格
adaptDarkModeboolfalse是否适配深色模式支持深色模式的 APP
darkBgColorColorColor(0xFF2C2C2C)深色模式背景色深色模式适配
darkBorderColorColorColor(0xFF444444)深色模式边框色深色模式适配
isLoadingboolfalse是否显示加载状态异步加载选项场景

三、生产级完整代码(可直接复制)

dart

import 'package:flutter/material.dart';
/// 通用下拉选择器(支持单选/多选、搜索筛选、全样式自定义、深色模式适配)
class CommonDropdown extends StatefulWidget {
  // 必选核心参数
  final List options; // 选项列表(不可为空)
  final ValueChanged onChanged; // 选择回调
  // 选中值(单选:String;多选:List)
  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 createState() => _CommonDropdownState();
}
class _CommonDropdownState extends State {
  // 搜索控制器(带防抖)
  final TextEditingController _searchController = TextEditingController();
  // 筛选后的选项列表
  late List _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 selectedList = widget.value is List
          ? 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 selectedList = widget.value is List
          ? List.from(widget.value)
          : [];
      return selectedList.contains(option);
    } else {
      return widget.value.toString() == option;
    }
  }
  /// 全选/取消全选逻辑
  void _toggleSelectAll() {
    final List allOptions = List.from(widget.options);
    final List currentSelected = widget.value is List
        ? 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 && (widget.value as List).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 newValues = widget.value is List
                                      ? 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 createState() => _FormGenderSelectState();
}
class _FormGenderSelectState extends State {
  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 createState() => _HobbySelectState();
}
class _HobbySelectState extends State {
  List _selectedHobbies = []; // 多选值:List类型
  @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 createState() => _CitySelectState();
}
class _CitySelectState extends State {
  String? _selectedCity;
  List _cityList = [];
  bool _isLoading = true;
  @override
  void initState() {
    super.initState();
    // 模拟接口请求加载城市列表
    _loadCityList();
  }
  Future _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 createState() => _CategoryFilterState();
}
class _CategoryFilterState extends State {
  List _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 createState() => _RegisterFormState();
}
class _RegisterFormState extends State {
  final _formKey = GlobalKey();
  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(
              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 options,
    required dynamic value,
    required ValueChanged 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 options,
    required dynamic value,
    required ValueChanged 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 _selectedCategories = [];
  List get selectedCategories => _selectedCategories;
  void setSelectedCategories(List value) {
    _selectedCategories = value;
    notifyListeners();
  }
  // 清空选中项
  void clearSelected() {
    _selectedCategories = [];
    notifyListeners();
  }
}
// 使用示例
class FilterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => FilterProvider(),
      child: Consumer(
        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,显示加载动画
深色模式样式冲突未开启adaptDarkMode1. 设置adaptDarkMode: true2. 配置深色模式样式参数
搜索高频触发筛选无防抖处理组件内置 300ms 防抖,减少性能消耗(已集成)

七、扩展能力(按需定制)

1. 自定义选项样式(带图标 / 颜色)

支持选项带图标、自定义选中样式:

dart

// 1. 扩展组件参数(新增图标配置)
final List? 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 newValues = widget.value is List
      ? 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),一起共建开源鸿蒙跨平台生态。

posted @ 2026-01-16 08:08  clnchanpin  阅读(13)  评论(0)    收藏  举报