Flutter DraggableScrollableSheet内嵌搜索栏无法实时过滤数据问题
问题根因
你遇到的搜索不实时生效问题,本质是showModalBottomSheet默认属于独立的路由上下文,你在父组件调用setState只能触发父组件的build方法重绘,不会主动刷新底部弹窗内的视图,只有当键盘收起触发父页面布局更新时,底部弹窗才会同步最新的foundCity数据,因此出现了延迟更新的现象。
解决方案
方案1:将底部弹窗抽为独立StatefulWidget(推荐)
将底部选择城市的逻辑独立封装,让弹窗自己管理搜索状态、城市列表数据,选中后通过回调将结果返回给父组件,可完全解决状态同步问题。
父组件中触发弹窗的代码修改如下:
GestureDetector( onTap: () => showModalBottomSheet( backgroundColor: Colors.transparent, isScrollControlled: true, context: context, builder: (context) => CitySelectSheet( cityList: cites, onSelected: (city) { setState(() { selected = city; }); }, ), ), // 原有容器展示代码保持不变 )
独立的城市选择弹窗组件代码:
class CitySelectSheet extends StatefulWidget { final List<City> cityList; final Function(City) onSelected; const CitySelectSheet({Key? key, required this.cityList, required this.onSelected}) : super(key: key); @override State<CitySelectSheet> createState() => _CitySelectSheetState(); } class _CitySelectSheetState extends State<CitySelectSheet> { final TextEditingController _searchController = TextEditingController(); List<City> _foundCity = []; @override void initState() { super.initState(); _foundCity = widget.cityList; _searchController.addListener(() { setState(() { final keyword = _searchController.text.toLowerCase(); _foundCity = widget.cityList.where((city) => city.name.toLowerCase().contains(keyword)).toList(); }); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Navigator.of(context).pop(), child: DraggableScrollableSheet( initialChildSize: 0.9, builder: (_, controller) => Container( padding: const EdgeInsets.fromLTRB(10, 10, 10, 0), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ Divider( thickness: 4, color: Colors.grey.shade300, endIndent: 170, indent: 170, ), const Padding( padding: EdgeInsets.only(bottom: 15, top: 5), child: Text("Select city", style: TextStyle( fontSize: 18, color: Colors.black, fontWeight: FontWeight.bold)), ), Padding( padding: const EdgeInsets.only(bottom: 10), child: TextFormField( controller: _searchController, keyboardType: TextInputType.name, style: TextStyle(fontSize: 16 * fontScale(context)), decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(7 * screenScale(context))), contentPadding: EdgeInsets.only(top: 10 * screenScale(context)), hintText: 'Search', prefixIcon: const Icon(Ionicons.search_outline), ), ), ), Expanded( child: _foundCity.isNotEmpty ? ListView.builder( itemCount: _foundCity.length, itemBuilder: (context, index) { final city = _foundCity[index]; return ListTile( title: Text(city.name), onTap: () { Navigator.of(context).pop(); widget.onSelected(city); }, ); }, ) : const Padding( padding: EdgeInsets.only(top: 50), child: Text( "No data.", style: TextStyle(fontSize: 16), ), ), ), ], ), ), ), ); } }
方案2:使用ValueNotifier局部刷新(无需拆分组件)
如果你不想拆分原有组件结构,可通过ValueNotifier实现局部数据监听刷新,不需要调用父组件setState也能更新弹窗内的列表:
- 首先将
foundCity修改为ValueNotifier类型,并修改初始化逻辑:
// 原有定义 List<City> foundCity = []; 替换为下方代码 final ValueNotifier<List<City>> foundCity = ValueNotifier([]); @override void initState() { super.initState(); foundCity.value = cites; }
- 修改
buildSheet方法内的搜索输入回调和列表渲染逻辑:
// TextFormField的onChanged修改为直接更新foundCity的value,不需要调用setState onChanged: (value) { foundCity.value = cites .where((city) => city.name.toLowerCase().contains(value.toLowerCase())) .toList(); }, // 列表部分用ValueListenableBuilder包裹,监听foundCity的变化自动刷新 Expanded( child: ValueListenableBuilder<List<City>>( valueListenable: foundCity, builder: (context, list, child) { return list.isNotEmpty ? ListView.builder( itemCount: list.length, itemBuilder: (context, index) { final city = list[index]; return ListTile( title: Text(city.name), onTap: () { Navigator.of(context).pop(); setState(() { selected = city; }); }, ); }, ) : const Padding( padding: EdgeInsets.only(top: 50), child: Text( "No data.", style: TextStyle(fontSize: 16), ), ); }, ), )
- 组件销毁时释放
ValueNotifier避免内存泄漏:
@override void dispose() { _search.dispose(); foundCity.dispose(); super.dispose(); }
内容的提问来源于stack exchange,提问作者MinhHy
相关产品推荐
相关产品推荐

