You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取Flutter ListView中各TextFormField的输入数据?

解决方案

1. 创建数据模型类

先定义一个PackageItem类,用来存储每个包裹表单的输入数据和对应的控制器,确保每个表单的数据相互独立:

class PackageItem {
  final TextEditingController quantityController;
  final TextEditingController weightController;
  final TextEditingController heightController;

  PackageItem()
      : quantityController = TextEditingController(),
        weightController = TextEditingController(),
        heightController = TextEditingController();

  // 销毁控制器,避免内存泄漏
  void dispose() {
    quantityController.dispose();
    weightController.dispose();
    heightController.dispose();
  }
}

2. 修改状态类变量

替换原来的全局变量和零散列表,改用包裹实例列表管理所有表单:

class _AddItemsToRequestState extends State<AddItemsToRequest> {
  List<PackageItem> packageItems = [];
  RegExp regExp = RegExp('[a-zA-Z]'); // 修正正则表达式,避免匹配非字母字符

  // ... 其他代码
}

3. 重构表单组件

修改itemForm方法,接收PackageItem参数,将输入框与对应实例的控制器绑定:

Widget itemForm(PackageItem item) {
  return Column(children: [
    Container(
        decoration:
            BoxDecoration(color: grey, border: Border.all(color: black)),
        width: double.infinity,
        child: const Center(
          child: Text('Package details',
              style: TextStyle(
                  fontWeight: FontWeight.bold,
                  backgroundColor: grey,
                  fontSize: 24)),
        )),
    Row(
      children: [
        Flexible(
          child: TextFormField(
            controller: item.quantityController,
            keyboardType: TextInputType.number,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Quantity cannot be empty';
              }
              if (int.tryParse(value) == null) {
                return 'Please enter a valid number';
              }
              return null;
            },
            autovalidateMode: AutovalidateMode.onUserInteraction,
            decoration: const InputDecoration(
                errorStyle: TextStyle(color: Colors.redAccent),
                border: OutlineInputBorder(
                  borderSide: BorderSide(),
                  borderRadius: BorderRadius.all(
                    Radius.circular(0.0),
                  ),
                ),
                fillColor: Color.fromARGB(255, 238, 238, 238),
                filled: true,
                focusedBorder: OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.blueAccent, width: 2.5),
                ),
                hintText: "Quantity : "),
          ),
        ),
        Flexible(
          child: TextFormField(
            controller: item.weightController,
            keyboardType: const TextInputType.numberWithOptions(decimal: true),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Weight cannot be empty';
              }
              if (regExp.hasMatch(value) || double.tryParse(value) == null) {
                return 'Please enter a valid number';
              }
              return null;
            },
            autovalidateMode: AutovalidateMode.onUserInteraction,
            decoration: const InputDecoration(
                errorStyle: TextStyle(color: Colors.redAccent),
                border: OutlineInputBorder(
                  borderSide: BorderSide(),
                  borderRadius: BorderRadius.all(
                    Radius.circular(0.0),
                  ),
                ),
                fillColor: Color.fromARGB(255, 238, 238, 238),
                filled: true,
                focusedBorder: OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.blueAccent, width: 2.5),
                ),
                hintText: "Weight : "),
          ),
        ),
        Flexible(
          child: TextFormField(
            controller: item.heightController,
            keyboardType: const TextInputType.numberWithOptions(decimal: true),
            validator: (value) => value!.isEmpty ? 'Height cannot be empty' : null,
            autovalidateMode: AutovalidateMode.onUserInteraction,
            decoration: const InputDecoration(
                errorStyle: TextStyle(color: Colors.redAccent),
                border: OutlineInputBorder(
                  borderSide: BorderSide(),
                  borderRadius: BorderRadius.all(
                    Radius.circular(0.0),
                  ),
                ),
                fillColor: Color.fromARGB(255, 238, 238, 238),
                filled: true,
                focusedBorder: OutlineInputBorder(
                  borderSide: BorderSide(color: Colors.blueAccent, width: 2.5),
                ),
                hintText: "Height : "),
          ),
        ),
      ],
    ),
  ]);
}

4. 修改添加按钮逻辑

点击添加按钮时,创建新的PackageItem实例并加入列表:

Widget _addTile() {
  return ElevatedButton(
      child: const Icon(Icons.add),
      onPressed: () {
        setState(() {
          packageItems.add(PackageItem());
        });
      });
}

5. 更新ListView构建

基于packageItems列表渲染表单,删除时同步销毁对应控制器:

Widget _listView() {
  return ListView.builder(
    scrollDirection: Axis.vertical,
    shrinkWrap: true,
    itemCount: packageItems.length,
    itemBuilder: (context, index) {
      final item = packageItems[index];
      return Dismissible(
        key: Key('package_$index'), // 使用索引生成稳定的key
        onDismissed: (direction) {
          setState(() {
            packageItems[index].dispose(); // 销毁控制器释放资源
            packageItems.removeAt(index);
          });
          ScaffoldMessenger.of(context)
              .showSnackBar(const SnackBar(content: Text('Package removed')));
        },
        background: Container(
            color: const Color.fromARGB(255, 210, 31, 19),
            child: const Center(
              child: Text(
                'Remove ',
                style: TextStyle(
                    color: white, fontWeight: FontWeight.bold, fontSize: 32),
              ),
            )),
        child: Container(
          width: double.infinity,
          margin: const EdgeInsets.all(5),
          child: itemForm(item),
        ),
      );
    },
  );
}

6. 获取所有表单数据并上传Firebase

在OK按钮点击事件中,遍历所有包裹实例,提取输入值后上传:

Widget _okButton() {
  return ElevatedButton(
    onPressed: () {
      // 收集所有有效包裹数据
      List<Map<String, dynamic>> allPackages = [];
      for (var item in packageItems) {
        int? quantity = int.tryParse(item.quantityController.text);
        double? weight = double.tryParse(item.weightController.text);
        double? height = double.tryParse(item.heightController.text);
        
        if (quantity != null && weight != null && height != null) {
          allPackages.add({
            'quantity': quantity,
            'weight': weight,
            'height': height,
          });
        }
      }

      // 此处添加Firebase上传逻辑,示例:
      // FirebaseFirestore.instance.collection('shipping_requests').add({'packages': allPackages});

      print(allPackages); // 测试输出
      Navigator.of(context).pop();
    },
    child: const Text("OK"),
  );
}

7. 页面销毁时清理资源

重写dispose方法,销毁所有控制器避免内存泄漏:

@override
void dispose() {
  for (var item in packageItems) {
    item.dispose();
  }
  super.dispose();
}

核心改进说明

  • 用独立的PackageItem实例管理每个表单的控制器和数据,避免全局变量覆盖问题
  • 通过TextEditingController直接获取输入值,无需依赖onChanged回调
  • 增加控制器销毁逻辑,避免内存泄漏
  • 优化输入验证,确保输入内容为有效数字

内容的提问来源于stack exchange,提问作者louks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 05:15:35