3、DropdownButtonFormField:下拉选择器的实现与数据绑定

下拉选择器,说白了就是那个让你点一下弹出一堆选项的组件。在表单里太常见了——选性别、选城市、选职位,几乎每个表单都有它。Flutter 官方提供了 DropdownButtonFormField,这东西比原生的 DropdownButton 好用不少,因为它天然支持表单校验和状态管理。

我个人习惯,只要是在 Form 里的下拉选择,一律用 DropdownButtonFormField。为什么?省事。它自带 validatoronSaved,跟 TextFormField 的用法几乎一样,学习成本很低。

3.1 基础用法:从静态数据开始

先看一个最简单的例子。假设我们要让用户选择性别:

DropdownButtonFormField<String>(
  decoration: InputDecoration(
    labelText: '性别',
    border: OutlineInputBorder(),
  ),
  value: _selectedGender,
  items: [
    DropdownMenuItem(value: 'male', child: Text('男')),
    DropdownMenuItem(value: 'female', child: Text('女')),
  ],
  onChanged: (value) {
    setState(() {
      _selectedGender = value;
    });
  },
)

嗯,这里要注意几点:

  • value 必须跟 DropdownMenuItemvalue 类型一致。我见过有人把 value 设成 null,但 items 里的 value 是字符串,结果一运行就报错。
  • onChanged 里一定要调 setState,否则界面不会刷新。这个坑我踩过,第一次写的时候忘了调,点了半天选项纹丝不动,还以为是组件坏了。
  • decorationTextFormField 完全一样,你可以加 hintTextprefixIcon 等等。
小技巧:如果想让下拉框默认显示一个提示文字(比如“请选择”),可以把 value 设为 null,然后在 items 里加一个 DropdownMenuItemvalue 也是 null,但 child 显示“请选择”。不过我个人更推荐用 hintText 来实现,更干净。

3.2 数据绑定:从 API 拿数据

实际项目中,下拉选项很少是写死的。大部分情况是从后端接口获取的。比如选择城市列表:

class CityPicker extends StatefulWidget {
  @override
  _CityPickerState createState() => _CityPickerState();
}

class _CityPickerState extends State<CityPicker> {
  List<City> _cities = [];
  City? _selectedCity;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _loadCities();
  }

  Future<void> _loadCities() async {
    setState(() => _isLoading = true);
    try {
      final response = await http.get(Uri.parse('https://api.example.com/cities'));
      final List<dynamic> data = json.decode(response.body);
      setState(() {
        _cities = data.map((json) => City.fromJson(json)).toList();
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
      // 处理错误
    }
  }

  @override
  Widget build(BuildContext context) {
    return DropdownButtonFormField<City>(
      value: _selectedCity,
      decoration: InputDecoration(
        labelText: '城市',
        border: OutlineInputBorder(),
      ),
      items: _cities.map((city) {
        return DropdownMenuItem<City>(
          value: city,
          child: Text(city.name),
        );
      }).toList(),
      onChanged: (value) {
        setState(() {
          _selectedCity = value;
        });
      },
    );
  }
}

这里有个关键点:DropdownButtonFormField 的泛型是 City,而不是 String。这意味着 valueitems 里的 value 都是 City 对象。这样做的好处是,你选中的不仅仅是一个字符串,而是整个对象,后续拿 _selectedCity.id 或者 _selectedCity.code 都很方便。

注意:如果 _cities 列表为空,DropdownButtonFormField 会报错。我曾经在项目里遇到过,接口返回了空数组,结果页面直接崩溃。解决方案很简单:在 items 为空时,显示一个 Text('暂无数据') 或者干脆隐藏这个下拉框。

3.3 表单校验:让用户不能乱选

下拉选择器也需要校验。比如用户必须选择一个城市,不能留空:

DropdownButtonFormField<City>(
  value: _selectedCity,
  decoration: InputDecoration(
    labelText: '城市',
    border: OutlineInputBorder(),
  ),
  items: _cities.map((city) {
    return DropdownMenuItem<City>(
      value: city,
      child: Text(city.name),
    );
  }).toList(),
  validator: (value) {
    if (value == null) {
      return '请选择一个城市';
    }
    return null;
  },
  onChanged: (value) {
    setState(() {
      _selectedCity = value;
    });
  },
)

校验逻辑跟 TextFormField 一模一样。你可以在 FormonSubmit 里统一调用 _formKey.currentState!.validate(),所有字段的校验会一起触发。

我个人习惯,在校验失败时,除了显示错误信息,还会把下拉框的边框变红。这个其实不用额外写代码,InputDecorationerrorBorderfocusedErrorBorder 会自动处理。

3.4 进阶技巧:搜索过滤与分组

当选项特别多的时候(比如全国城市列表,几百个),用户一个个翻就太痛苦了。这时候可以加一个搜索框来过滤选项。

实现思路很简单:维护一个 _filteredCities 列表,根据用户输入的搜索词动态过滤。但要注意,DropdownButtonFormField 本身不支持搜索,你需要自己写一个搜索框放在下拉框上面,或者用第三方库。

还有一种场景是分组。比如选择商品分类,一级分类下面有二级分类。Flutter 官方没有提供分组下拉组件,但你可以用 PopupMenuButton 配合 PopupMenuDivider 来实现。不过说实话,这种复杂场景我建议直接用 showModalBottomSheet 弹出一个列表,体验更好。

避坑指南:我曾经在项目里用 DropdownButtonFormField 做省市区三级联动,结果发现它不支持嵌套。后来改用 CupertinoPicker 才搞定。所以如果你的需求是级联选择,别硬磕 DropdownButtonFormField,换个思路。

3.5 性能优化:别让列表卡顿

如果下拉选项有成百上千个,直接渲染所有 DropdownMenuItem 会导致界面卡顿。为什么?因为 DropdownButtonFormField 会把所有 items 一次性构建出来,即使它们没有显示。

解决方案有两个:

  • 懒加载:只在用户点击下拉框时才加载数据。比如在 onTap 回调里请求接口。
  • 虚拟列表:ListView.builder 替代 DropdownMenuItem 列表。但 DropdownButtonFormField 不支持直接替换,你需要自己实现一个自定义下拉组件。

我个人建议,如果选项超过 50 个,就别用 DropdownButtonFormField 了。改用 Autocomplete 或者 SearchDelegate,用户体验更好,性能也更强。

3.6 总结

DropdownButtonFormField 是 Flutter 表单里最常用的组件之一。它简单、易用,跟 Form 配合得天衣无缝。但别把它神化,遇到复杂场景(级联、搜索、大量数据)时,该换就换。

最后说一句:下拉选择器的核心不是 UI,而是数据绑定。把数据模型设计好,后面的代码写起来就顺了。我见过太多人把 value 设成字符串,结果后面要拿 ID 还得再查一遍列表,何必呢?