3、DropdownButtonFormField:下拉选择器的实现与数据绑定
下拉选择器,说白了就是那个让你点一下弹出一堆选项的组件。在表单里太常见了——选性别、选城市、选职位,几乎每个表单都有它。Flutter 官方提供了 DropdownButtonFormField,这东西比原生的 DropdownButton 好用不少,因为它天然支持表单校验和状态管理。
我个人习惯,只要是在 Form 里的下拉选择,一律用 DropdownButtonFormField。为什么?省事。它自带 validator、onSaved,跟 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必须跟DropdownMenuItem的value类型一致。我见过有人把value设成null,但items里的value是字符串,结果一运行就报错。onChanged里一定要调setState,否则界面不会刷新。这个坑我踩过,第一次写的时候忘了调,点了半天选项纹丝不动,还以为是组件坏了。decoration跟TextFormField完全一样,你可以加hintText、prefixIcon等等。
value 设为 null,然后在 items 里加一个 DropdownMenuItem 的 value 也是 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。这意味着 value 和 items 里的 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 一模一样。你可以在 Form 的 onSubmit 里统一调用 _formKey.currentState!.validate(),所有字段的校验会一起触发。
我个人习惯,在校验失败时,除了显示错误信息,还会把下拉框的边框变红。这个其实不用额外写代码,InputDecoration 的 errorBorder 和 focusedErrorBorder 会自动处理。
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 还得再查一遍列表,何必呢?