Flutter动态表单中DropdownButton选中值控制与移除问题
- 开发动态添加分组字段的Flutter表单,初始仅显示添加按钮,用户可添加任意多组表单,每组包含两个
TextFormField和一个DropdownButton - 现有问题:添加4组表单填写内容后,移除第二组时,
ListView会错误删除最后一项,但列表数据已在选中索引处正确移除;同时DropdownButton的选中值无法与数据同步,移除项后状态混乱 - 已完成:
TextFormField的控制器列表管理及销毁逻辑
现有代码
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: const Purchased(), ); } } 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() { textControllerList.forEach((element) { element.dispose(); }); textControllerList1.forEach((element) { element.dispose(); }); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: () { list.add(UserInfo()); 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), Text('category'), Text(list[index].category) ], ); })), ), Expanded( child: ListView.builder( shrinkWrap: true, itemCount: list.length, itemBuilder: ((context, index) { TextEditingController controller = TextEditingController(); TextEditingController controller1 = TextEditingController(); textControllerList.add(controller); textControllerList1.add(controller1); return MyForm( textEditingController: textControllerList[index], textEditingController1: textControllerList1[index], key: ValueKey(index), initInfo: list[index], onChangePhone: (phoneVal) { if (phoneVal != null) { setState(() { list[index].setPhone(phoneVal); }); } }, 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); textControllerList1.removeAt(index); 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.onremove, required this.textEditingController, required this.textEditingController1, }); @override State<MyForm> createState() => _MyFormState(); } class _MyFormState extends State<MyForm> { List<UserInfo> list = <UserInfo>[]; final List<String> category = [ 'Manager', 'Reception', 'Sales', 'Service', ]; @override void initState() { super.initState(); } @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, ), DropdownButton( isExpanded: true, hint: const Text( 'Select Category', style: TextStyle(fontSize: 14), ), icon: const Icon( Icons.arrow_drop_down, color: Colors.black45, ), iconSize: 30, items: category .map((item) => DropdownMenuItem<String>( value: item, child: Text( item, style: const TextStyle( fontSize: 14, ), ), )) .toList(), onChanged: widget.onChangeCategory) ], ), ); } } class UserInfo { String _phone = ''; String _email = ''; String _category = ''; 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; } }
解决方案
修改后完整代码
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, ), home: const Purchased(), ); } } 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> phoneControllers = []; List<TextEditingController> emailControllers = []; @override void dispose() { phoneControllers.forEach((controller) => controller.dispose()); emailControllers.forEach((controller) => controller.dispose()); super.dispose(); } void _addNewForm() { setState(() { list.add(UserInfo()); phoneControllers.add(TextEditingController()); emailControllers.add(TextEditingController()); }); } void _removeForm(int index) { setState(() { list.removeAt(index); phoneControllers.removeAt(index).dispose(); emailControllers.removeAt(index).dispose(); }); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton( onPressed: _addNewForm, child: const Icon(Icons.add), ), body: Column( children: [ Expanded( child: ListView.builder( shrinkWrap: true, itemCount: list.length, itemBuilder: (context, index) { final info = list[index]; return Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Phone:'), Text(info.phone), const SizedBox(height: 4), const Text('Email:'), Text(info.email), const SizedBox(height: 4), const Text('Category:'), Text(info.category.isEmpty ? '未选择' : info.category), const Divider(), ], ), ); }, ), ), Expanded( child: ListView.builder( shrinkWrap: true, itemCount: list.length, itemBuilder: (context, index) { return MyForm( key: ObjectKey(list[index]), initInfo: list[index], phoneController: phoneControllers[index], emailController: emailControllers[index], onPhoneChanged: (value) { if (value != null) { list[index].setPhone(value); } }, onEmailChanged: (value) { if (value != null) { list[index].setEmail(value); } }, onCategoryChanged: (value) { if (value != null) { setState(() { list[index].setCategory(value); }); } }, onRemove: () => _removeForm(index), ); }, ), ), ], ), ); } } class MyForm extends StatelessWidget { final UserInfo initInfo; final TextEditingController phoneController; final TextEditingController emailController; final Function(String?) onPhoneChanged; final Function(String?) onEmailChanged; final Function(String?) onCategoryChanged; final VoidCallback onRemove; final List<String> categories = [ 'Manager', 'Reception', 'Sales', 'Service', ]; const MyForm({ super.key, required this.initInfo, required this.phoneController, required this.emailController, required this.onPhoneChanged, required this.onEmailChanged, required this.onCategoryChanged, required this.onRemove, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ Align( alignment: Alignment.centerRight, child: IconButton( onPressed: onRemove, icon: const Icon(Icons.remove_circle, color: Colors.red), ), ), TextFormField( controller: phoneController, onChanged: onPhoneChanged, decoration: const InputDecoration( labelText: 'Phone', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextFormField( controller: emailController, onChanged: onEmailChanged, decoration: const InputDecoration( labelText: 'Email', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), DropdownButtonFormField<String>( isExpanded: true, value: initInfo.category.isEmpty ? null : initInfo.category, hint: const Text('Select Category'), icon: const Icon(Icons.arrow_drop_down, color: Colors.black45), decoration: const InputDecoration( border: OutlineInputBorder(), ), items: categories.map((category) { return DropdownMenuItem( value: category, child: Text(category), ); }).toList(), onChanged: onCategoryChanged, ), ], ), ); } } class UserInfo { String _phone = ''; String _email = ''; String _category = ''; 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; }
核心修改说明
修复ListView项删除错误
- 将
ValueKey(index)替换为ObjectKey(list[index]),用数据对象作为唯一标识,避免索引变化导致的Widget复用错误 - 提取添加/删除表单的逻辑为单独函数,避免build方法内重复创建控制器
- 将
DropdownButton状态同步
- 使用
DropdownButtonFormField替代原生DropdownButton,贴合表单场景 - 绑定
value属性到initInfo.category,确保下拉框显示当前选中值 - 在
onChanged回调中调用setState,保证数据更新后UI同步
- 使用
优化控制器管理
- 添加新表单时同步创建对应控制器,避免在
itemBuilder中重复添加 - 删除表单时立即销毁对应控制器,防止内存泄漏
- 添加新表单时同步创建对应控制器,避免在
内容的提问来源于stack exchange,提问作者Mohammed Bekele
相关产品推荐
相关产品推荐

