Flutter中点击TextFormField为何触发整个表单重渲染?
问题分析与解决方案
问题根源
- Stream重复创建:每次页面build时,
groceryItem函数都会生成新的Firestore Stream实例,StreamBuilder检测到Stream变化后会重新发起监听,导致页面反复进入加载状态。 - 控制器文本被强制重置:在StreamBuilder的builder回调中,每次有数据返回都会直接给
_nameController.text赋值,覆盖用户正在输入的内容,同时触发页面重绘,形成恶性循环。 - 键盘弹出触发重绘:点击TextFormField弹出键盘时,
MediaQuery.of(context).viewInsets发生变化,触发页面build,进一步加剧上述问题。
修复步骤
- 将Stream的创建移到
initState中,确保组件生命周期内只创建一次Stream。 - 添加初始化标记,仅在首次获取数据时初始化控制器文本,避免覆盖用户输入。
- 移除builder中重复的控制器赋值逻辑。
修改后的完整代码
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; class CrudPage2 extends StatefulWidget { final String docId; const CrudPage2({Key? key, required this.docId}) : super(key: key); @override CrudPage2State createState() => CrudPage2State(); } class CrudPage2State extends State<CrudPage2> { late String name = ""; late bool isImportant = false; late Stream<DocumentSnapshot<Object?>> _groceryStream; bool _isInitialized = false; final TextEditingController _nameController = TextEditingController(); final TextEditingController _importantController = TextEditingController(); @override void initState() { super.initState(); // 仅在初始化时创建一次Stream _groceryStream = FirebaseFirestore.instance .collection("groceries") .doc(widget.docId) .snapshots(); // 处理新增数据的初始化 if (widget.docId == "NEW") { _nameController.text = ""; _importantController.text = "false"; _isInitialized = true; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), title: const Text("Grocery Item"), ), body: SizedBox( width: double.infinity, child: Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom + 20), child: StreamBuilder<DocumentSnapshot>( stream: _groceryStream, // 使用初始化好的固定Stream builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> streamSnapshot) { if (streamSnapshot.connectionState == ConnectionState.waiting) { print("****** Loading ******"); return const Text("Loading"); } else if (streamSnapshot.hasData) { // 仅首次获取数据时初始化控制器和状态变量 if (!_isInitialized && widget.docId != "NEW") { var jsonData = streamSnapshot.data?.data(); Map<String, dynamic> myData = jsonData as Map<String, dynamic>; setState(() { name = myData['name']; isImportant = myData['important']; _nameController.text = name; _importantController.text = isImportant ? "true" : "false"; _isInitialized = true; }); } return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), ), ), StatefulBuilder( builder: (BuildContext context, StateSetter importantStateSetter) { return Row( children: [ const Text("Important: "), Switch( value: isImportant, onChanged: (value) { importantStateSetter(() => isImportant = value); }, ), ], ); }, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( child: ElevatedButton( child: const Text('Cancel'), onPressed: () async { Navigator.of(context).pop(); }), ), const SizedBox(width: 10), SizedBox( child: ElevatedButton( child: const Text("Submit"), onPressed: () async { final String name = _nameController.text; if (widget.docId == 'NEW') { addGroceryItem(name, 1.0, "test", isImportant); } else { updateGroceryItem(widget.docId, name, 1.0, "test", isImportant); } _nameController.text = ''; _importantController.text = ""; Navigator.of(context).pop(); }, ), ) ], ), ], ); } else { return const Text("No Data"); } } ), ), ), ); } Future<void> addGroceryItem( String name, double quantity, String category, bool isImportant) async { await FirebaseFirestore.instance.collection('groceries').add({ "active": true, "name": name, "quantity": quantity, "category": category, "important": isImportant }); } Future<void> updateGroceryItem(String docID, String name, double quantity, String category, bool isImportant) async { await FirebaseFirestore.instance.collection('groceries').doc(docID).update({ "active": true, "name": name, "quantity": quantity, "category": category, "important": isImportant }); } }
额外说明
- 移除了原有的
groceryItem函数,改为在initState中直接创建Stream,避免每次build生成新实例。 - 新增
_isInitialized标记,确保控制器仅被初始化一次,不会覆盖用户输入。 - 处理了新增数据(docId为"NEW")的初始化逻辑,避免不必要的Stream监听。
内容的提问来源于stack exchange,提问作者SqueezeOJ
相关产品推荐
相关产品推荐

