Flutter动态ListView删除选中项异常:误删最后一项求助
问题根源分析
- 索引作为
ValueKey的隐患:用列表索引作为ValueKey时,删除中间元素后,后续元素的索引会前移,Flutter会复用之前的Widget实例,导致旧的状态(如下拉框选中值)被错误绑定到新的数据项上。 initState仅初始化一次:MyForm的initState仅在Widget创建时执行,后续父组件更新initInfo时,子组件的控制器和下拉框选中状态不会同步更新。- 控制器管理错误:新尝试代码中在
ListView.builder的itemBuilder里每次创建并添加控制器到列表,build方法会多次执行,导致控制器列表冗余、状态混乱。
解决方案
1. 给数据模型添加唯一标识
修改UserInfo类,添加唯一id字段,确保每个数据项有独立身份:
class UserInfo { final String id; // 新增唯一标识 String _phone = ''; String _email = ''; String _category = ''; UserInfo() : id = UniqueKey().toString(); // 初始化时生成唯一id String get phone => _phone; String get email => _email; String get category => _category; void setPhone(String phone) => _phone = phone; void setEmail(String email) => _email = email; void setCategory(String category) => _category = category; }
2. 用数据项的唯一id作为ValueKey
在ListView.builder中,将ValueKey(index)改为ValueKey(list[index].id),确保每个MyFormWidget对应唯一数据项:
return MyForm( key: ValueKey(list[index].id), // 改用数据项的唯一id // ...其他参数 );
3. 同步更新子组件状态
在MyForm的State类中,重写didUpdateWidget方法,当父组件传递的initInfo变化时,同步更新控制器和下拉框选中值:
@override void didUpdateWidget(covariant MyForm oldWidget) { super.didUpdateWidget(oldWidget); // 当数据模型变化时,同步更新控制器和选中值 if (oldWidget.initInfo != widget.initInfo) { widget.textEditingController.text = widget.initInfo.phone; widget.textEditingController1.text = widget.initInfo.email; setState(() { selected = widget.initInfo.category; }); } }
4. 修正控制器管理逻辑
将控制器的创建和管理绑定到UserInfo的添加动作,避免在build中重复创建:
// 在PurchasedState的浮动按钮点击事件中 floatingActionButton: FloatingActionButton( onPressed: () { final userInfo = UserInfo(); list.add(userInfo); textControllerList.add(TextEditingController(text: userInfo.phone)); textControllerList1.add(TextEditingController(text: userInfo.email)); setState(() {}); }, child: const Icon(Icons.add), ),
修改后的完整代码
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; class Purchased extends StatefulWidget { const Purchased({Key? key}) : super(key: key); @override State<Purchased> createState() => _PurchasedState(); } class _PurchasedState extends State<Purchased> { List<UserInfo> list = []; List<TextEditingController> textControllerList = []; List<TextEditingController> textControllerList1 = []; @override void dispose() { for (var controller in textControllerList) { controller.dispose(); } for (var controller in textControllerList1) { controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { final userInfo = UserInfo(); list.add(userInfo); textControllerList.add(TextEditingController(text: userInfo.phone)); textControllerList1.add(TextEditingController(text: userInfo.email)); setState(() {}); }, child: const Icon(Icons.add), ), body: Column( children: [ Expanded( child: ListView.builder( shrinkWrap: true, itemCount: list.length, itemBuilder: ((context, index) { return Column( children: [ const Text('phone'), Text(list[index].phone), const Text('email'), Text(list[index].email), const Text('category'), Text(list[index].category) ], ); }), ), ), Expanded( child: ListView.builder( shrinkWrap: true, itemCount: list.length, itemBuilder: ((context, index) { return MyForm( key: ValueKey(list[index].id), textEditingController: textControllerList[index], textEditingController1: textControllerList1[index], initInfo: list[index], onChangePhone: (phoneVal) { if (phoneVal != null) { list[index].setPhone(phoneVal); setState(() {}); } }, onchangeEmail: (emailVal) { if (emailVal != null) { list[index].setEmail(emailVal); setState(() {}); } }, onchangeCategory: (categoryVal) { if (categoryVal != null) { list[index].setCategory(categoryVal); setState(() {}); } }, onremove: () { list.removeAt(index); textControllerList.removeAt(index).dispose(); textControllerList1.removeAt(index).dispose(); setState(() {}); }, ); }), ), ) ], ), ); } } class MyForm extends StatefulWidget { final UserInfo initInfo; final Function(String?) onChangePhone; final Function(String?) onchangeEmail; final Function(String?) onchangeCategory; final TextEditingController textEditingController; final TextEditingController textEditingController1; final VoidCallback? onremove; const MyForm({ super.key, required this.initInfo, required this.onChangePhone, required this.onchangeEmail, required this.onchangeCategory, required this.textEditingController, required this.textEditingController1, required this.onremove, }); @override State<MyForm> createState() => _MyFormState(); } class _MyFormState extends State<MyForm> { String? selected; final List<String> category = [ 'Manager', 'Reception', 'Sales', 'Service', ]; @override void initState() { super.initState(); selected = widget.initInfo.category; // 初始化控制器文本 widget.textEditingController.text = widget.initInfo.phone; widget.textEditingController1.text = widget.initInfo.email; } @override void didUpdateWidget(covariant MyForm oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.initInfo != widget.initInfo) { // 同步更新控制器和选中值 widget.textEditingController.text = widget.initInfo.phone; widget.textEditingController1.text = widget.initInfo.email; setState(() { selected = widget.initInfo.category; }); } } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), child: Column( children: [ IconButton(onPressed: widget.onremove, icon: const Icon(Icons.remove)), TextFormField( controller: widget.textEditingController, onChanged: widget.onChangePhone, ), TextFormField( controller: widget.textEditingController1, onChanged: widget.onchangeEmail, ), DropdownButtonFormField2( decoration: InputDecoration( isDense: true, contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(5), ), ), isExpanded: true, hint: const Text( 'Select Category', style: TextStyle(fontSize: 14), ), icon: const Icon( Icons.arrow_drop_down, color: Colors.black45, ), iconSize: 30, buttonHeight: 60, value: selected?.isEmpty == true ? null : selected, buttonPadding: const EdgeInsets.only(left: 20, right: 10), items: category .map((item) => DropdownMenuItem<String>( value: item, child: Text( item, style: const TextStyle(fontSize: 14), ), )) .toList(), validator: (value) { if (value == null) { return 'Please select Category.'; } return null; }, onChanged: widget.onchangeCategory, onSaved: widget.onchangeCategory, ) ], ), ); } } class UserInfo { final String id; String _phone = ''; String _email = ''; String _category = ''; UserInfo() : id = UniqueKey().toString(); String get phone => _phone; String get email => _email; String get category => _category; void setPhone(String phone) => _phone = phone; void setEmail(String email) => _email = email; void setCategory(String category) => _category = category; }
关键改动说明
- 给
UserInfo添加唯一id,避免用索引作为Widget标识,彻底解决Widget复用错误。 - 重写
didUpdateWidget,确保数据变化时子组件的控制器和下拉框状态同步更新。 - 修正控制器创建逻辑,仅在添加数据项时创建控制器,删除时同步销毁,避免内存泄漏和状态混乱。
- 移除冗余的
listCtrl变量,简化状态管理。
内容的提问来源于stack exchange,提问作者mobix
相关产品推荐
相关产品推荐

