Flutter中ExpansionTile内TextField输入单字符后失焦问题求助
问题分析与解决
核心问题
你遇到的输入一个字符就失去焦点的问题,根源有两个:
- TextEditingController重复创建:在
ListView.builder的itemBuilder内部每次都会新建TextEditingController,当你输入字符触发Firestore更新后,流数据变化会导致列表重新build,此时每个item都会重新创建controller,原有的输入状态和焦点被重置。 - 频繁触发重建:controller的
addListener会在每输入一个字符就调用Firestore更新,这会导致流数据频繁刷新,进而触发列表频繁重建,加剧焦点丢失的问题。
解决步骤
1. 改用Map存储Controller(避免重复创建)
在有状态组件的initState中初始化一个Map,用来保存每个shoppingListId对应的TextEditingController,这样每次列表重建时,不会重新创建controller,而是复用已有的实例。
2. 添加防抖逻辑(减少Firestore更新频率)
直接在controller监听里调用Firestore更新会导致每输入一个字符就发起一次请求,既影响性能又会频繁触发组件重建。可以用debounce来延迟执行更新,比如等待用户停止输入300ms后再更新数据库。
3. 优化组件结构
确保你的组件是有状态组件,这样controller的状态能被正确保留,不会在重建时丢失。
修改后的代码示例
// 首先确保这是一个有状态组件 class YourShoppingListWidget extends StatefulWidget { // 你的widget参数 final List<ShoppingListModel> shoppingListsCart; final String shoppingCartListId; final FirebaseCloudStorage firebaseCloudStorage; const YourShoppingListWidget({ super.key, required this.shoppingListsCart, required this.shoppingCartListId, required this.firebaseCloudStorage, }); @override State<YourShoppingListWidget> createState() => _YourShoppingListWidgetState(); } class _YourShoppingListWidgetState extends State<YourShoppingListWidget> { // 用Map存储每个shoppingListId对应的controller final Map<String, TextEditingController> _controllers = {}; // 防抖定时器 Timer? _debounceTimer; @override void initState() { super.initState(); // 初始化所有controller for (var list in widget.shoppingListsCart) { _controllers[list.shoppingListId] = TextEditingController(text: list.name); // 给每个controller添加监听 _controllers[list.shoppingListId]?.addListener(() { _onListNameChanged(list.shoppingListId); }); } } @override void didUpdateWidget(covariant YourShoppingListWidget oldWidget) { super.didUpdateWidget(oldWidget); // 当shoppingListsCart更新时,同步更新controller for (var list in widget.shoppingListsCart) { if (!_controllers.containsKey(list.shoppingListId)) { _controllers[list.shoppingListId] = TextEditingController(text: list.name); _controllers[list.shoppingListId]?.addListener(() { _onListNameChanged(list.shoppingListId); }); } else if (_controllers[list.shoppingListId]?.text != list.name) { // 避免本地输入被数据库数据覆盖 _controllers[list.shoppingListId]?.text = list.name; } } // 移除已删除列表的controller _controllers.removeWhere((key, value) => !widget.shoppingListsCart.any((list) => list.shoppingListId == key)); } @override void dispose() { // 销毁所有controller for (var controller in _controllers.values) { controller.dispose(); } _debounceTimer?.cancel(); super.dispose(); } void _onListNameChanged(String shoppingListId) { final controller = _controllers[shoppingListId]; if (controller == null) return; // 防抖:取消之前的定时器,重新计时 _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 300), () { widget.firebaseCloudStorage.updateShoppingListName( widget.shoppingCartListId, shoppingListId, controller.text, ); }); } @override Widget build(BuildContext context) { return SizedBox( width: double.infinity, child: SingleChildScrollView( child: Column( children: [ ListView.builder( physics: const ClampingScrollPhysics(), shrinkWrap: true, itemCount: widget.shoppingListsCart.length, itemBuilder: (BuildContext context, int index) { final shoppingList = widget.shoppingListsCart[index]; final controller = _controllers[shoppingList.shoppingListId]; return ExpansionTile( backgroundColor: Colors.indigo, title: SizedBox( child: Row( children: [ SizedBox( width: 220, child: TextField( decoration: const InputDecoration( border: InputBorder.none, counterText: '', ), controller: controller, maxLength: 35, ), ), Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ IconButton( onPressed: () {}, icon: const Icon(Icons.edit), ), IconButton( onPressed: () {}, icon: const Icon(Icons.delete), ), ], ), ], ), ), children: [ ProductsInShoppingListView( shoppingListName: shoppingList.name, shoppingCartId: widget.shoppingCartListId, shoppingListId: shoppingList.shoppingListId, firebaseCloudStorage: widget.firebaseCloudStorage, ), ], ); }, ), ProductsInCartView( shoppingCartId: widget.shoppingCartListId, ), ], ), ), ); } }
关键改动说明
- Controller复用:用Map存储每个列表项的controller,在
initState和didUpdateWidget中初始化和更新,避免每次build都新建。 - 防抖逻辑:通过
Timer延迟执行Firestore更新,减少请求频率和组件重建次数。 - 生命周期管理:在
dispose中销毁所有controller和定时器,避免内存泄漏。 - 数据同步:在
didUpdateWidget中处理列表数据更新时的controller同步,避免本地输入被覆盖。
内容的提问来源于stack exchange,提问作者jorge
相关产品推荐
相关产品推荐

