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

Flutter:点击TextFormField时FutureBuilder重复请求Firebase数据问题

问题:FutureBuilder在输入框获焦时重复发起Firestore请求

我构建了如下组件结构:一个StatefulWidget(HandleOrderScreen)中使用FutureBuilder调用FirestoreService().getOrderPriceUserBoat(id)从Firebase获取数据,数据返回后渲染包含两个TextFormField的Stateful组件TextFormFieldWidget。

原本离线状态下组件运行正常,但出现问题:当TextFormFieldWidget中的任意输入框获取焦点时,Future会重新从Firebase拉取数据。虽未触发组件重建、状态得以保留,但每次点击输入框都发起不必要的Firestore请求,我希望仅在用户进入页面时请求一次数据,在停留期间复用该快照。

我无法理解为何未触发重建时FutureBuilder仍会重复请求数据。相关代码如下:

HandleOrderScreen代码

import 'package:flutter/material.dart';
import 'package:MyApp/manage_orders/handle/widgets/form_has_handling.dart';
import 'package:MyApp/services/firestore.dart';
import 'package:MyApp/services/models.dart';

class HandleOrderScreen extends StatefulWidget {
  static const routeName = '/handle-order-screen';

  @override
  State<HandleOrderScreen> createState() => _HandleOrderScreenState();
}

class _HandleOrderScreenState extends State<HandleOrderScreen> {
  @override
  Widget build(BuildContext context) {
    final String id = ModalRoute.of(context)!.settings.arguments as String;

    return FutureBuilder<Map>(
        future: FirestoreService().getOrderPriceUserBoat(id),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Scaffold(
              appBar: AppBar(
                title: Text(''),
              ),
              body: Center(
                child: Center(child: CircularProgressIndicator()),
              ),
            );
          } else if (snapshot.hasError) {
            return Scaffold(
              appBar: AppBar(
                title: Text('Error'),
              ),
              body: Center(
                child: Text(snapshot.error.toString()),
              ),
            );
          } else if (snapshot.hasData) {
            var snapshotData = snapshot.data;
            Order order = snapshotData!['order'];

            return TextFormFieldWidget(order: order);
          } else {
            return Scaffold(
              appBar: AppBar(
                title: Text('Error'),
              ),
              body: Center(
                child: Text('Something went wrong'),
              ),
            );
          }
        });
  }
}

TextFormFieldWidget代码

import 'package:flutter/material.dart';
import 'package:MyApp/services/models.dart';

class TextFormFieldWidget extends StatefulWidget {
  const TextFormFieldWidget({
    Key? key,
    required this.order,
  }) : super(key: key);

  final Order order;

  @override
  State<TextFormFieldWidget> createState() => _TextFormFieldWidgetState();
}

class _TextFormFieldWidgetState extends State<TextFormFieldWidget> {
  final commentController = TextEditingController();
  final priceController = TextEditingController();
  static final formkey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;

    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: Text('Handle'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Container(
              padding: EdgeInsets.all(5),
              height: size.height - 120,
              child: Form(
                key: formkey,
                child: Column(
                  children: [
                    Container(
                      padding: EdgeInsets.all(2),
                      width: double.infinity,
                      height: 120,
                      child: Card(
                        color: Color.fromARGB(255, 241, 235, 235),
                        child: Container(
                          child: RichText(
                            text: TextSpan(
                              style:
                                  TextStyle(color: Colors.black, fontSize: 20),
                              children: <TextSpan>[
                                TextSpan(
                                  text: 'Test Name\n',
                                ),
                                TextSpan(
                                  text: 'Order.nr:',
                                  style: TextStyle(fontWeight: FontWeight.bold),
                                ),
                                TextSpan(
                                  text: '${widget.order.orderNumber} \n',
                                ),
                                TextSpan(
                                  text: 'Car: ',
                                  style: TextStyle(fontWeight: FontWeight.bold),
                                ),
                                TextSpan(
                                  text: widget.order.carModel + '\n',
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                    Container(
                      padding: EdgeInsets.all(2),
                      child: Align(
                        alignment: Alignment.topLeft,
                        child: Text(
                          " Type:",
                          style: TextStyle(color: Colors.grey),
                        ),
                      ),
                    ),
                    SizedBox(
                      height: 10,
                    ),
                    Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: Colors.black, width: 2),
                      ),
                      child: Container(
                        padding: EdgeInsets.all(5),
                        child: TextFormField(
                          controller: priceController,
                          decoration: InputDecoration(labelText: 'Price: '),
                          validator: (value) {
                            if (value != null) {
                              if (value.length < 1) {
                                return 'Wright the price';
                              } else {
                                return null;
                              }
                            }
                          },
                          keyboardType: TextInputType.number,
                          style: TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 20),
                        ),
                      ),
                    ),
                    SizedBox(
                      height: 10,
                    ),
                    Container(
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(10),
                        border: Border.all(color: Colors.black, width: 2),
                      ),
                      child: Container(
                        padding: EdgeInsets.all(5),
                        child: TextFormField(
                          controller: commentController,
                          decoration:
                              InputDecoration(labelText: 'Description:'),
                          keyboardType: TextInputType.multiline,
                          style: TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 20),
                          minLines: 2,
                          maxLines: 5,
                          validator: (value) {
                            if (value != null) {
                              if (value.length < 10) {
                                return 'Give a short description';
                              } else {
                                return null;
                              }
                            }
                          },
                        ),
                      ),
                    ),
                    Container(
                      padding: EdgeInsets.all(5),
                      child: ElevatedButton(
                        child: Text('Send in'),
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                            fixedSize: Size(size.width, 50),
                            primary: Color.fromARGB(255, 223, 208, 0)),
                      ),
                    ),
                    Spacer(),
                    Container(
                      padding: EdgeInsets.all(5),
                      child: ElevatedButton(
                        child: Text('Finish order'),
                        onPressed: () {},
                        style: ElevatedButton.styleFrom(
                            fixedSize: Size(size.width, 50),
                            primary: Color.fromARGB(255, 255, 17, 0)),
                      ),
                    ),
                  ],
                ),
              ),
            ),
            SizedBox(
              height: 200,
            ),
          ],
        ),
      ),
    );
  }
}

原因分析

问题出在build方法内创建Future:每次build执行时,都会重新调用FirestoreService().getOrderPriceUserBoat(id)生成新的Future实例。输入框获焦会触发组件树重绘(比如键盘弹出导致布局变化、输入框状态更新触发父组件build),此时FutureBuilder检测到传入的Future是新对象,就会重新执行异步请求。

解决方案

将Future的创建移到State的初始化阶段,保存Future实例,避免每次build都生成新对象。修改后的HandleOrderScreen代码如下:

import 'package:flutter/material.dart';
import 'package:MyApp/manage_orders/handle/widgets/form_has_handling.dart';
import 'package:MyApp/services/firestore.dart';
import 'package:MyApp/services/models.dart';

class HandleOrderScreen extends StatefulWidget {
  static const routeName = '/handle-order-screen';

  @override
  State<HandleOrderScreen> createState() => _HandleOrderScreenState();
}

class _HandleOrderScreenState extends State<HandleOrderScreen> {
  late Future<Map> _orderFuture;
  late String _orderId;

  @override
  void initState() {
    super.initState();
    // 在组件初始化阶段获取参数并创建Future,仅执行一次
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _orderId = ModalRoute.of(context)!.settings.arguments as String;
      _orderFuture = FirestoreService().getOrderPriceUserBoat(_orderId);
    });
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Map>(
        future: _orderFuture, // 使用保存的Future实例
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Scaffold(
              appBar: AppBar(
                title: Text(''),
              ),
              body: Center(
                child: CircularProgressIndicator(),
              ),
            );
          } else if (snapshot.hasError) {
            return Scaffold(
              appBar: AppBar(
                title: Text('Error'),
              ),
              body: Center(
                child: Text(snapshot.error.toString()),
              ),
            );
          } else if (snapshot.hasData) {
            var snapshotData = snapshot.data;
            Order order = snapshotData!['order'];

            return TextFormFieldWidget(order: order);
          } else {
            return Scaffold(
              appBar: AppBar(
                title: Text('Error'),
              ),
              body: Center(
                child: Text('Something went wrong'),
              ),
            );
          }
        });
  }
}

修改说明

  • 在State类中声明_orderFuture和_orderId,用于保存Future实例和订单ID
  • 通过initState中的addPostFrameCallback获取路由参数(避免initState阶段context无法访问ModalRoute的问题),并初始化Future,此过程仅在组件创建时执行一次
  • build方法直接复用保存的_orderFuture,不再每次生成新的Future对象

这样就能确保Firestore请求仅在页面进入时发起一次,后续输入框获焦或其他重绘操作不会重复触发请求。

内容的提问来源于stack exchange,提问作者cueless

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 11:25:27