Flutter TextField选中/取消选中触发组件重建,重复执行Future问题
Flutter 搜索页面键盘弹出/收起时重复触发Future问题
场景设置
我在路由栈中推入一个简易搜索页面,页面包含TextField和返回按钮。点击返回按钮可弹出当前页面,用户执行搜索操作时,组件会重建并展示搜索结果,该功能目前运行正常。
问题描述
当前遇到的问题是:每当选中或取消选中TextField(即键盘弹出或收起)时,整个组件会触发重建,进而重复执行Future,即使搜索词未发生变化,页面也会显示进度圈直至Future返回结果。
我查阅相关资料后得知MediaQuery.of(context)可能是引发重建的原因,但我代码中大量使用该方法,因此希望找到无需移除它的替代解决方案来避免上述问题。
感谢您的帮助!
相关代码
// 推入带状态的Widget Navigator.push( context, MaterialPageRoute( builder: (context) => SearchRoomBuilder( key: const Key('SearchRoomBuilder'), searchTerm: value, ), ), ); class SearchRoomBuilder extends Stateful Widget { final String searchTerm; const SearchRoomBuilder({super.key, required this.searchTerm}); @override State<SearchRoomBuilder> createState() => _SearchRoomBuilderState(); } class _SearchRoomBuilderState extends State<SearchRoomBuilder> with AutomaticKeepAliveClientMixin { late final TextEditingController _textController; final double _borderRadius = 20.0; late String searchTerm; static final GlobalKey<FormFieldState> _searchFieldKey = GlobalKey<FormFieldState>(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _textController = TextEditingController(text: widget.searchTerm); searchTerm = widget.searchTerm; } @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); final theme = Theme.of(context); print('$searchTerm'); return Scaffold( body: Column( children: [ Container( height: MediaQuery.of(context).viewPadding.top, ), Card( margin: const EdgeInsets.fromLTRB(0, 0, 0, 0), elevation: theme.appBarTheme.elevation, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0.0), ), child: Container( height: 60, width: MediaQuery.of(context).size.width, color: theme.appBarTheme.backgroundColor, child: Row( children: [ InkWell( child: Container( width: 60, height: 60, padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.arrow_back_rounded, color: theme.appBarTheme.foregroundColor, ), ), onTap: () => Navigator.of(context).pop(), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: TextField( key: _searchFieldKey, controller: _textController, enableSuggestions: false, autocorrect: false, textAlign: TextAlign.start, textAlignVertical: TextAlignVertical.center, keyboardType: TextInputType.text, style: theme.textTheme.titleMedium, decoration: InputDecoration( prefixIcon: Icon( Icons.search, color: theme.colorScheme.onBackground.withOpacity(0.5), ), contentPadding: EdgeInsets.all(10.0), hintText: 'Search', hintStyle: theme.textTheme.titleMedium?.apply( color: theme.colorScheme.onBackground .withOpacity(0.5)), ), onSubmitted: (value) { setState(() { searchTerm = value; _textController.text = value; }); }, ), )), ], ), ), ), Expanded( child: FutureBuilder( key: Key('$searchTerm'), future: MManager().searchQuery(searchTerm), builder: (context, AsyncSnapshot<List<Product>?> snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError || ((snapshot.data?.length ?? -1) <= 0) || snapshot.data == null) { //If no results found for any reason display no results message //CODE THAT DISPLAYS NO RESULT MESSAGE HERE } else { return ListView.separated( itemCount: snapshot.data?.length ?? 0, padding: EdgeInsets.symmetric(vertical: 10), separatorBuilder: (context, index) { return SizedBox(height: 5); }, itemBuilder: (context, index) { if (snapshot.data?[index] == null) { return const SizedBox.shrink(); } return DisplayListTile( product: snapshot.data![index], index: index, ); }, ); } } else { //While waiting for respons return Center( child: CircularProgressIndicator( color: theme.colorScheme.primary, )); } }, ), ), ], ), ); } }
解决方案
核心问题分析
键盘弹出/收起时,MediaQuery的viewInsets会变化,导致依赖它的组件重建,但这不是重复请求的根本原因——你在build方法中直接调用MManager().searchQuery(searchTerm),每次组件重建都会创建新的Future,FutureBuilder会重新执行这个Future,才导致了不必要的重复请求。
具体修复步骤
- 缓存Future实例:在State类中维护一个
Future类型的变量,只在搜索词真正变化时重新赋值,避免每次build都创建新的Future。 - 优化状态更新逻辑:仅当用户提交新的搜索词时,才更新缓存的Future,触发新的搜索请求。
修改后的代码示例
class _SearchRoomBuilderState extends State<SearchRoomBuilder> with AutomaticKeepAliveClientMixin { late final TextEditingController _textController; final double _borderRadius = 20.0; late String searchTerm; // 新增:缓存搜索请求的Future late Future<List<Product>?> _searchFuture; static final GlobalKey<FormFieldState> _searchFieldKey = GlobalKey<FormFieldState>(); @override bool get wantKeepAlive => true; @override void initState() { super.initState(); _textController = TextEditingController(text: widget.searchTerm); searchTerm = widget.searchTerm; // 初始化搜索请求 _searchFuture = MManager().searchQuery(searchTerm); } // 可选:处理父组件传递的searchTerm变化 @override void didUpdateWidget(covariant SearchRoomBuilder oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.searchTerm != widget.searchTerm) { searchTerm = widget.searchTerm; _textController.text = searchTerm; _searchFuture = MManager().searchQuery(searchTerm); } } @override void dispose() { _textController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); final theme = Theme.of(context); print('$searchTerm'); return Scaffold( body: Column( children: [ Container( height: MediaQuery.of(context).viewPadding.top, ), Card( margin: const EdgeInsets.fromLTRB(0, 0, 0, 0), elevation: theme.appBarTheme.elevation, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(0.0), ), child: Container( height: 60, width: MediaQuery.of(context).size.width, color: theme.appBarTheme.backgroundColor, child: Row( children: [ InkWell( child: Container( width: 60, height: 60, padding: const EdgeInsets.symmetric(horizontal: 8), child: Icon( Icons.arrow_back_rounded, color: theme.appBarTheme.foregroundColor, ), ), onTap: () => Navigator.of(context).pop(), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: TextField( key: _searchFieldKey, controller: _textController, enableSuggestions: false, autocorrect: false, textAlign: TextAlign.start, textAlignVertical: TextAlignVertical.center, keyboardType: TextInputType.text, style: theme.textTheme.titleMedium, decoration: InputDecoration( prefixIcon: Icon( Icons.search, color: theme.colorScheme.onBackground.withOpacity(0.5), ), contentPadding: EdgeInsets.all(10.0), hintText: 'Search', hintStyle: theme.textTheme.titleMedium?.apply( color: theme.colorScheme.onBackground .withOpacity(0.5)), ), onSubmitted: (value) { setState(() { searchTerm = value; _textController.text = value; // 仅当搜索词变化时,更新Future触发新请求 _searchFuture = MManager().searchQuery(searchTerm); }); }, ), )), ], ), ), ), Expanded( child: FutureBuilder( key: Key('$searchTerm'), // 使用缓存的Future,而非每次build创建新实例 future: _searchFuture, builder: (context, AsyncSnapshot<List<Product>?> snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError || ((snapshot.data?.length ?? -1) <= 0) || snapshot.data == null) { return Center(child: Text('无搜索结果')); } else { return ListView.separated( itemCount: snapshot.data?.length ?? 0, padding: EdgeInsets.symmetric(vertical: 10), separatorBuilder: (context, index) { return SizedBox(height: 5); }, itemBuilder: (context, index) { if (snapshot.data?[index] == null) { return const SizedBox.shrink(); } return DisplayListTile( product: snapshot.data![index], index: index, ); }, ); } } else { return Center( child: CircularProgressIndicator( color: theme.colorScheme.primary, )); } }, ), ), ], ), ); } }
额外优化建议
- 如果需要实现实时搜索(输入过程中触发搜索),可以添加防抖逻辑,避免输入时频繁发起请求
- 可以使用
ValueNotifier或ChangeNotifier管理搜索状态,进一步减少不必要的组件重建
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

