You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,才导致了不必要的重复请求。

具体修复步骤

  1. 缓存Future实例:在State类中维护一个Future类型的变量,只在搜索词真正变化时重新赋值,避免每次build都创建新的Future。
  2. 优化状态更新逻辑:仅当用户提交新的搜索词时,才更新缓存的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 09:04:59