Flutter:点击TextFormField时FutureBuilder重复请求Firebase数据问题
我构建了如下组件结构:一个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

