Flutter 动态Widget列表实现添加与删除表单行的最佳方案是什么
最优实现方案
原有代码存在3个核心问题:
- 直接在State中缓存Row Widget列表,且在build方法中反复追加元素,会导致重复渲染的bug
- 未维护表单数据源与TextField输入状态,删除/新增行时用户已输入的内容会丢失
- 动态生成的行未指定唯一Key,列表刷新时会出现Widget复用混乱
核心改造思路
- 数据源与视图分离:在State中维护表单数据源,每个条目绑定对应的
TextEditingController来持有TextField的输入值 - 动态生成视图:每次build时直接根据当前数据源生成行Widget,不做Widget缓存
- 操作直接作用于数据源:新增/删除操作仅修改数据源,调用
setState触发视图自动刷新 - 资源释放:在dispose生命周期释放所有
TextEditingController避免内存泄漏
完整改造代码
import 'package:flutter/material.dart'; class EditJdataForm2 extends StatefulWidget { final tags = <MapEntry<String, String>>{ const MapEntry('city', 'Chicago'), const MapEntry('name', 'John'), const MapEntry('fullname', 'Doe'), const MapEntry('age', '30') }; @override _EditJdataForm2State createState() => _EditJdataForm2State(); } class _EditJdataForm2State extends State<EditJdataForm2> { // 维护表单数据源,每个条目绑定文本控制器持有输入值 final List<MapEntry<String, TextEditingController>> _formDataSource = []; @override void initState() { super.initState(); // 初始化数据源,绑定控制器 for (var tag in widget.tags) { _formDataSource.add(MapEntry( tag.key, TextEditingController(text: tag.value), )); } } @override void dispose() { // 释放所有控制器,避免内存泄漏 for (var entry in _formDataSource) { entry.value.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { // 每次build动态生成行列表,不做缓存 List<Widget> formRows = _formDataSource.asMap().entries.map((item) { int index = item.key; var entry = item.value; return Row( // 给每一行加唯一Key,避免Widget复用混乱 key: ValueKey('${entry.key}_$index'), children: [ Expanded( flex: 3, child: Container( margin: const EdgeInsets.only(left: 30.0, right: 30.0), child: Text(entry.key, style: const TextStyle( color: Colors.black54, fontSize: 20.0, letterSpacing: 0, fontWeight: FontWeight.normal, )), ), ), Expanded( flex: 6, child: Container( margin: const EdgeInsets.only(right: 30.0), child: TextFormField( // 绑定控制器持有输入值 controller: entry.value, ))), Container( margin: const EdgeInsets.only(right: 30.0), child: Ink( decoration: const ShapeDecoration( color: Colors.red, shape: CircleBorder(), ), child: IconButton( icon: const Icon( Icons.remove, color: Colors.white, ), onPressed: () { // 删除指定行:释放控制器+移除数据源+刷新视图 setState(() { _formDataSource[index].value.dispose(); _formDataSource.removeAt(index); }); }, ), ), ), ], ); }).toList(); // 加入新增按钮 formRows.add( Container( margin: const EdgeInsets.only(top: 20, left: 30, right: 30), width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Icons.add), label: const Text('新增一行'), onPressed: () { // 新增行:加入新的数据源条目+刷新视图 setState(() { int newIndex = _formDataSource.length; _formDataSource.add(MapEntry( 'newTag$newIndex', TextEditingController(text: ''), )); }); }, ), ), ); return SingleChildScrollView( child: Column(children: formRows), ); } // 可调用该方法获取最终表单提交数据 Map<String, String> getFormData() { Map<String, String> result = {}; for (var entry in _formDataSource) { result[entry.key] = entry.value.text; } return result; } }
补充说明
- 如果需要自定义新增行的Label,可在新增按钮点击时弹出输入框让用户输入自定义Key,再加入数据源即可
- 如果需要支持编辑Label,可把展示Key的Text组件换成TextField,同样绑定对应的控制器即可
- 提交表单时调用
getFormData()方法即可直接获取所有字段的键值对
内容的提问来源于stack exchange,提问作者Jan Domeyko
相关产品推荐
相关产品推荐

