Flutter中PopupMenu内ListView无法更新过滤数据问题
解决Flutter CustomDropdown搜索过滤后ListView不更新的问题
问题根源
你使用showMenu弹出的下拉菜单,其内容是在调用showMenu时一次性构建的快照。后续调用setState更新filteredItems只会刷新CustomDropdown组件本身,不会重新渲染已经弹出的菜单内容,因此ListView无法同步显示过滤后的条目。
解决方案一:用StatefulBuilder实现菜单内部状态更新
将搜索框和过滤列表放到StatefulBuilder内部,让菜单拥有独立的状态更新能力,搜索时直接刷新菜单内的UI:
import 'package:flutter/material.dart'; class CustomDropdown extends StatefulWidget { final String labelText; final List<String> items; final ValueChanged<String?> onChanged; final TextEditingController dropdownTextFieldController; const CustomDropdown({ Key? key, required this.labelText, required this.items, required this.onChanged, required this.dropdownTextFieldController, }) : super(key: key); @override CustomDropdownState createState() => CustomDropdownState(); } class CustomDropdownState extends State<CustomDropdown> { TextEditingController searchController = TextEditingController(); void _showDropdown(BuildContext context) { final RenderBox textFieldRenderBox = context.findRenderObject() as RenderBox; List<String> tempFilteredItems = List.from(widget.items); showMenu( context: context, position: RelativeRect.fromLTRB( textFieldRenderBox.localToGlobal(Offset.zero).dx, textFieldRenderBox.localToGlobal(Offset.zero).dy + textFieldRenderBox.size.height, textFieldRenderBox.localToGlobal(Offset.zero).dx + textFieldRenderBox.size.width, textFieldRenderBox.localToGlobal(Offset.zero).dy + textFieldRenderBox.size.height + 10, ), items: <PopupMenuEntry<String>>[ PopupMenuItem<String>( child: StatefulBuilder( builder: (menuContext, setStateMenu) { return SizedBox( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: searchController, decoration: const InputDecoration( hintText: 'Search...', suffixIcon: Icon(Icons.search), ), onChanged: (query) { setStateMenu(() { tempFilteredItems = widget.items .where((item) => item.toLowerCase().contains(query.toLowerCase())) .toList(); }); }, ), if (tempFilteredItems.isNotEmpty) SizedBox( height: 200, width: 300, child: ListView.builder( itemCount: tempFilteredItems.length, itemBuilder: (BuildContext context, int index) { return ListTile( title: Text(tempFilteredItems[index]), onTap: () { widget.dropdownTextFieldController.text = tempFilteredItems[index]; widget.onChanged(tempFilteredItems[index]); Navigator.pop(context); }, ); }, ), ), ], ), ); }, ), ), ], ).then((value) { if (value != null) { widget.dropdownTextFieldController.text = value; widget.onChanged(value); } searchController.clear(); }); } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( widget.labelText, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 10), TextField( readOnly: true, onTap: () => _showDropdown(context), controller: widget.dropdownTextFieldController, decoration: InputDecoration( hintText: "Select an item", suffixIcon: IconButton( icon: const Icon(Icons.arrow_drop_down), onPressed: () => _showDropdown(context), ), ), ), ], ); } } class CustomDropDown { Widget simpleDropDown( bool loading, String label, List<String> items, TextEditingController controller, Function? function, { bool showSearch = false, }) { return SizedBox( width: 300, child: CustomDropdown( labelText: label, items: items, onChanged: (value) => function?.call(value), dropdownTextFieldController: controller, ), ); } } class Components { bool isCompanyLoading = false; List<String> listOfCompany = []; TextEditingController companyController = TextEditingController(); void comIdDd(String? value) {} Widget customdd() { return CustomDropDown().simpleDropDown( isCompanyLoading, 'Company', listOfCompany, companyController, comIdDd, ); } }
关键修改点
- 移除原有的
filteredItems变量和filterItems方法,将过滤逻辑移到菜单内部 - 用
StatefulBuilder给菜单内部提供状态更新能力,搜索时调用setStateMenu刷新ListView - 菜单关闭时清空搜索框内容,优化用户体验
- 删除重复定义的
CustomDropDown类,避免编译错误
方案二:改用Dialog实现更灵活的可搜索下拉框
如果showMenu限制较多,可以改用Dialog实现,Dialog本身是Stateful组件,无需额外处理状态:
void _showDropdown(BuildContext context) { List<String> tempFilteredItems = List.from(widget.items); showDialog( context: context, builder: (dialogContext) { return AlertDialog( contentPadding: EdgeInsets.zero, content: SizedBox( width: 300, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: searchController, decoration: const InputDecoration( hintText: 'Search...', suffixIcon: Icon(Icons.search), ), onChanged: (query) { setState(() { tempFilteredItems = widget.items .where((item) => item.toLowerCase().contains(query.toLowerCase())) .toList(); }); }, ), ), if (tempFilteredItems.isNotEmpty) SizedBox( height: 200, child: ListView.builder( itemCount: tempFilteredItems.length, itemBuilder: (BuildContext context, int index) { return ListTile( title: Text(tempFilteredItems[index]), onTap: () { widget.dropdownTextFieldController.text = tempFilteredItems[index]; widget.onChanged(tempFilteredItems[index]); Navigator.pop(dialogContext); }, ); }, ), ), ], ), ), ); }, ).then((_) => searchController.clear()); }
内容的提问来源于stack exchange,提问作者Taha Waseem
相关产品推荐
相关产品推荐

