Flutter动态表单数据获取及本地数据库存储问题
Flutter动态表单:获取字段值并本地存储
核心问题分析
原代码存在几个关键问题导致无法有效获取所有表单值:
- 局部变量(如
_mySelection)无法跨组件保留状态 - Radio组件使用全局变量
ids,无法区分不同字段组的选中状态 - TextField未关联状态存储,输入值无法被捕获
- 缺少统一的表单数据存储结构,难以批量处理和存储
解决方案实现
以下是重构后的完整代码,包含表单值的统一管理和本地存储功能:
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; // 引入本地存储库 class DynamicForm extends StatefulWidget { // 修正类名拼写(fromdata → DynamicForm) const DynamicForm({Key? key}) : super(key: key); @override _DynamicFormState createState() => _DynamicFormState(); } class _DynamicFormState extends State<DynamicForm> { // 统一存储所有表单字段的值,key为字段id,value为对应输入值 final Map<String, dynamic> _formValues = {}; var url = Uri.parse('https://stoplight.io/mocks/khurramsoftware/data/20414976/getdata'); // 获取API数据 Future<dynamic> apirequest() async { final response = await http.get(url); if (response.statusCode == 200) { return json.decode(response.body); } else { throw Exception(response.reasonPhrase); } } // 保存表单数据到本地 Future<void> _saveFormData() async { final prefs = await SharedPreferences.getInstance(); // 将Map转为JSON字符串存储 await prefs.setString('form_data', json.encode(_formValues)); // 提示保存成功 ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('表单数据已保存')), ); } // 构建动态表单行 Widget _buildFormRow(Map<String, dynamic> field) { final fieldId = field["id"]; final fieldType = field["type"]; final fieldName = field["name"]; final fieldOptions = field["options"] ?? []; switch (fieldType) { case "select": return Container( padding: const EdgeInsets.fromLTRB(28, 15, 28, 28), child: InputDecorator( decoration: InputDecoration( labelText: fieldName, focusColor: Colors.blue, enabledBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(10.0), ), border: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(10.0), ), focusedBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(20.0), ), ), child: DropdownButton<dynamic>( style: const TextStyle(color: Colors.deepOrangeAccent), hint: Text("选择 $fieldName"), value: _formValues[fieldId], // 从全局Map获取当前值 items: fieldOptions.map((option) { return DropdownMenuItem( child: Text(option["text"]), value: option["value"], ); }).toList(), onChanged: (newVal) { setState(() { _formValues[fieldId] = newVal; // 更新全局Map }); }, ), ), ); case "radio": return Container( padding: const EdgeInsets.fromLTRB(28, 15, 28, 28), child: InputDecorator( decoration: InputDecoration( labelText: fieldName, enabledBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(10.0), ), border: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(20.0), ), focusedBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(20.0), ), ), child: Column( children: fieldOptions.map((option) { final index = fieldOptions.indexOf(option); return RadioListTile<int>( title: Text(option["text"]), groupValue: _formValues[fieldId], // 从全局Map获取当前选中索引 activeColor: Colors.lightBlue, value: index, onChanged: (val) { setState(() { _formValues[fieldId] = val; // 更新全局Map }); }, ); }).toList(), ), ), ); default: // text/number/phone类型 return Padding( padding: const EdgeInsets.fromLTRB(28, 15, 28, 28), child: TextFormField( keyboardType: (fieldType == "number" && fieldId == "amount") ? TextInputType.numberWithOptions(decimal: true) : fieldType == "text" ? TextInputType.text : TextInputType.phone, inputFormatters: [ (fieldType == "number" && fieldId == "amount") ? FilteringTextInputFormatter.allow(RegExp("[.0-9]")) : fieldType == "text" ? FilteringTextInputFormatter.allow(RegExp("[a-zA-Z á-ú Á-Ú 0-9]")) : FilteringTextInputFormatter.digitsOnly, ], decoration: InputDecoration( labelText: fieldName, enabledBorder: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(10.0), ), border: OutlineInputBorder( borderSide: const BorderSide(color: Colors.deepOrangeAccent), borderRadius: BorderRadius.circular(10.0), ), ), initialValue: _formValues[fieldId], // 设置初始值 onChanged: (val) { _formValues[fieldId] = val; // 实时更新全局Map }, ), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("动态表单"), actions: [ // 添加保存按钮 IconButton( icon: const Icon(Icons.save), onPressed: _saveFormData, ) ], ), body: SingleChildScrollView( physics: const ScrollPhysics(), child: FutureBuilder( future: apirequest(), builder: (context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return const Center(child: Text('加载失败')); } if (snapshot.hasData) { return Column( children: [ const SizedBox(height: 40), Container( height: 120, child: CircleAvatar( radius: 80, backgroundImage: NetworkImage(snapshot.data['image_url']), ), ), const SizedBox(height: 20), Text(snapshot.data['service_name']), const SizedBox(height: 5), ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: snapshot.data['fields'].length, itemBuilder: (context, index) { return _buildFormRow(snapshot.data['fields'][index]); }, ), ], ); } return const Padding( padding: EdgeInsets.all(20.0), child: CircularProgressIndicator(), ); }, ), ), ); } }
关键改进点
- 统一状态管理:用
_formValuesMap存储所有字段值,key为字段id,确保每个字段的状态独立且可追踪 - Radio组件修复:每个Radio组的选中值单独存储在Map中,避免全局变量冲突
- TextField值捕获:通过
onChanged实时更新Map,无需维护大量TextEditingController - 本地存储实现:使用
shared_preferences将表单数据转为JSON字符串存储,也可根据需求替换为sqflite等数据库 - 代码结构优化:将表单行构建逻辑改为接收完整字段对象,提升可读性和维护性
使用说明
- 首先在
pubspec.yaml中添加依赖:
dependencies: flutter: sdk: flutter shared_preferences: ^2.2.2 http: ^1.1.0
- 运行
flutter pub get安装依赖 - 使用
DynamicForm组件即可实现动态表单的渲染、值捕获和本地存储
内容的提问来源于stack exchange,提问作者Abdul Karim
相关产品推荐
相关产品推荐

