如何获取ListView.builder循环内TextFormField的输入值
ListView.builder 中使用 TextFormField 绑定列表数据取值方案
核心问题本质
ListView 是懒加载回收机制,列表项滚动出可视区域后会被销毁,进入可视区域时会重新构建,直接在构建方法里临时创建输入框控制器、不做状态和数据源的绑定,必然会出现输入内容错位、滚动丢失、取值错乱的问题。
必守实现规则
- 每个输入框必须配置基于业务对象唯一ID的
ValueKey,禁止直接用列表索引作为Key,避免列表增删、滚动时状态复用错位 - 禁止在
itemBuilder构建方法内临时创建TextEditingController,控制器的生命周期要和页面/业务对象绑定,避免频繁创建销毁导致内容丢失 - 输入值必须和数据源对象的属性做双向绑定,不要依赖控件临时状态存值
两种常用场景实现
场景1:轻量输入,无需统一表单校验
适合只需要收集输入值、不需要做统一必填/格式校验的场景,直接给每个业务对象绑定专属控制器,实时同步输入值。
首先定义业务数据结构:
class BusinessItem { /// 业务对象唯一标识,绝对不能用索引代替 final String uniqueId; /// 初始为空字符串,存储用户输入内容 String inputValue; /// 绑定对应输入框的控制器 TextEditingController? editingController; BusinessItem({ required this.uniqueId, this.inputValue = '', this.editingController, }); }
页面核心实现代码:
class ListInputDemo extends StatefulWidget { const ListInputDemo({super.key}); @override State<ListInputDemo> createState() => _ListInputDemoState(); } class _ListInputDemoState extends State<ListInputDemo> { late final List<BusinessItem> _dataList; @override void initState() { super.initState(); // 初始化模拟数据,所有inputValue初始为空字符串 _dataList = List.generate(30, (index) => BusinessItem(uniqueId: 'biz_$index')); // 为每个业务对象初始化专属控制器,绑定初始空值 for (final item in _dataList) { item.editingController = TextEditingController(text: item.inputValue); // 监听输入,实时同步值到业务对象属性 item.editingController!.addListener(() { item.inputValue = item.editingController!.text; }); } } @override void dispose() { // 页面销毁时统一释放所有控制器,避免内存泄漏 for (final item in _dataList) { item.editingController?.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('列表输入 demo')), body: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _dataList.length, itemBuilder: (context, index) { final item = _dataList[index]; return Container( margin: const EdgeInsets.only(bottom: 12), child: TextFormField( key: ValueKey(item.uniqueId), controller: item.editingController, decoration: const InputDecoration( hintText: '请输入内容', border: OutlineInputBorder(), ), ), ); }, ), floatingActionButton: FloatingActionButton( onPressed: () { // 提交时直接遍历数据源,取每个对象的inputValue即可 final submitData = _dataList.map((e) => e.inputValue).toList(); debugPrint('收集到的所有输入值:$submitData'); // 后续写业务提交逻辑即可 }, child: const Icon(Icons.done), ), ); } }
场景2:需要统一表单校验
适合需要做必填校验、输入格式校验,统一提交的场景,用Form组件做全局管理,不需要提前初始化一堆控制器。
核心实现片段:
/// 表单全局key final _formKey = GlobalKey<FormState>(); late final List<BusinessItem> _dataList = List.generate(30, (index) => BusinessItem(uniqueId: 'biz_$index')); @override Widget build(BuildContext context) { return Scaffold( body: Form( key: _formKey, child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _dataList.length, itemBuilder: (context, index) { final item = _dataList[index]; return Container( margin: const EdgeInsets.only(bottom: 12), child: TextFormField( key: ValueKey(item.uniqueId), initialValue: item.inputValue, decoration: const InputDecoration( hintText: '请输入内容', border: OutlineInputBorder(), ), // 校验逻辑 validator: (val) { if (val == null || val.trim().isEmpty) { return '输入内容不能为空'; } return null; }, // 实时同步输入值到对象 onChanged: (val) => item.inputValue = val, ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { // 统一校验 if (_formKey.currentState!.validate()) { _formKey.currentState!.save(); // 校验通过后,_dataList里的inputValue已经是最新值,直接拿去提交即可 final submitData = _dataList.map((e) => e.inputValue).toList(); debugPrint('校验通过,提交数据:$submitData'); } }, child: const Icon(Icons.done), ), ); }
避坑提醒
- 如果列表支持动态增删项,增删数据时要同步处理对应控制器的创建和释放,保证控制器和业务对象一一对应
- 不要为了省事儿给所有输入框共用同一个控制器,会导致所有输入框内容同步覆盖
- 长列表场景优先用第二种方案,减少控制器初始化的内存开销
内容的提问来源于stack exchange,提问作者Houssem
相关产品推荐
相关产品推荐

