Flutter复用同一表单实现文档新增与编辑的方案问询
Flutter表单复用:同时支持新增与编辑文档操作
核心结论
需要设置状态标识区分新增/编辑模式,通过一个布尔变量(如isEditing)即可实现,复用同一表单组件,仅根据状态差异调整数据填充、按钮行为和API调用。
具体实现步骤
1. 添加状态标识与编辑数据存储
在页面State类中添加核心变量,控制表单模式并存储编辑数据:
bool isEditing = false; Document? editingDocument; // 存储待编辑的文档对象 // 页面构造函数,接收外部传递的模式和数据 YourFormPage({this.isEditing = false, this.editingDocument});
在initState中初始化表单,编辑模式下自动填充已有数据:
@override void initState() { super.initState(); if (isEditing && editingDocument != null) { docTitleController.text = editingDocument!.docTitle; tokenNoController.text = editingDocument!.tokenNo.toString(); durationController.text = editingDocument!.duration; startDateController.text = editingDocument!.startDate; endDateController.text = editingDocument!.endDate; } }
2. 动态调整表单行为
调整按钮文本
根据模式动态切换按钮显示文本:
child: Text(isEditing ? "Update" : "Save"),
区分保存/更新逻辑
在按钮onPressed中根据isEditing调用不同API:
onPressed: () { if (formKey.currentState!.validate()) { final String docTitle = docTitleController.text; final int tokenNo = int.parse(tokenNoController.text); final String duration = durationController.text; final String startDate = startDateController.text; final String endDate = endDateController.text; setState(() { if (isEditing) { // 编辑模式:调用更新接口,传入文档ID _futureDocument = updateDocument( editingDocument!.docId, docTitle, tokenNo, duration, startDate, endDate, ); } else { // 新增模式:调用原创建接口 _futureDocument = createDocument( docTitle, tokenNo, duration, startDate, endDate, ); } }); // 跳转回文档列表页(用pushReplacement避免返回重复页面) Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => DocumentPage(this.userName), )); } },
可选:控制字段可编辑状态
如果某些字段在编辑时需要禁用(比如Token No),通过enabled属性控制:
TextFormField( enabled: !isEditing, // 编辑模式下禁用该字段 // 其他原有属性保持不变 )
3. 完整修改后的表单代码
Form buildColumn() { return Form( key: formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ SizedBox(height: 5), Align( alignment: Alignment.topLeft, child: SizedBox( width: MediaQuery.of(context).size.width * 0.95, height: MediaQuery.of(context).size.height * 0.06, child: TextFormField( inputFormatters: [ FilteringTextInputFormatter.allow( RegExp("[ ',-/ a-z A-Z á-ú Á-Ú 0-9]"), ), ], controller: docTitleController, style: TextStyle(fontSize: 12), decoration: const InputDecoration( errorStyle: TextStyle(fontSize: 0.05), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(30)), ), hintStyle: TextStyle(fontSize: 12), labelStyle: TextStyle(fontSize: 12), labelText: 'Document Title', hintText: 'Document title required', ), validator: MultiValidator([RequiredValidator(errorText: 'Required*')]), ), ), ), SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Align( alignment: Alignment.topLeft, child: SizedBox( height: MediaQuery.of(context).size.height * 0.06, width: MediaQuery.of(context).size.width * 0.4, child: TextFormField( enabled: !isEditing, // 编辑模式下禁用Token No style: TextStyle(fontSize: 12), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(14) ], controller: tokenNoController, keyboardType: TextInputType.number, decoration: const InputDecoration( errorStyle: TextStyle(fontSize: 0.05), counterText: "", border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(30)), ), focusColor: Color.fromARGB(255, 3, 87, 156), hintStyle: TextStyle(fontSize: 12), labelStyle: TextStyle(fontSize: 12), labelText: 'Token No', hintText: 'Token no required', ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter token No'; } else if (value.length < 14) { return 'Please enter 14 digits number'; } return null; }, ), ), ), SizedBox(width: MediaQuery.of(context).size.width * 0.01), Align( alignment: Alignment.topLeft, child: SizedBox( height: MediaQuery.of(context).size.height * 0.06, width: MediaQuery.of(context).size.width * 0.4, child: TextFormField( style: TextStyle(fontSize: 12), inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(2) ], controller: durationController, keyboardType: TextInputType.number, decoration: const InputDecoration( errorStyle: TextStyle(fontSize: 0.05), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(30)), ), hintStyle: TextStyle(fontSize: 12), labelStyle: TextStyle(fontSize: 12), labelText: 'Duration(M)', hintText: 'Duration required', ), validator: MultiValidator([RequiredValidator(errorText: 'Required*')]), ), ), ), ], ), SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Align( alignment: Alignment.topLeft, child: SizedBox( height: MediaQuery.of(context).size.height * 0.06, width: MediaQuery.of(context).size.width * 0.4, child: TextFormField( style: TextStyle(fontSize: 12), inputFormatters: [ FilteringTextInputFormatter.allow(RegExp("[- 0-9]")), LengthLimitingTextInputFormatter(10) ], controller: startDateController, keyboardType: TextInputType.datetime, decoration: const InputDecoration( errorStyle: TextStyle(fontSize: 0.05), prefixIcon: Icon(Icons.calendar_month), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(30)), ), hintStyle: TextStyle(fontSize: 12), labelStyle: TextStyle(fontSize: 12), labelText: 'Start Date', hintText: 'yyyy-MM-dd', ), onTap: () async { DateTime? pickedDate = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(1991), lastDate: DateTime(2101), ).then((pickedDate) { if (pickedDate != null) { String formattedDate = DateFormat('yyyy-MM-dd').format(pickedDate); setState(() { startDateController.text = formattedDate; }); } }); if (durationController.text.isNotEmpty && startDateController.text.isNotEmpty) { final int dur = int.parse(durationController.text); var stDate = DateTime.parse(startDateController.text); var jiffy = Jiffy(stDate).add(months: dur, days: -1); String s = jiffy.format('yyyy-MM-dd'); setState(() { endDateController.text = s; }); } }, validator: MultiValidator([RequiredValidator(errorText: 'Required*')]), ), ), ), SizedBox(width: MediaQuery.of(context).size.width * 0.02), Align( alignment: Alignment.topLeft, child: SizedBox( height: MediaQuery.of(context).size.height * 0.06, width: MediaQuery.of(context).size.width * 0.4, child: TextFormField( style: TextStyle(fontSize: 12), autofocus: false, controller: endDateController, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp("[- 0-9]")), LengthLimitingTextInputFormatter(10) ], keyboardType: TextInputType.datetime, decoration: const InputDecoration( errorStyle: TextStyle(fontSize: 0.05), counterText: "", hoverColor: Color.fromARGB(255, 3, 87, 156), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(30)), borderSide: BorderSide(color: Color.fromARGB(255, 3, 87, 156)), ), hintStyle: TextStyle(fontSize: 12), labelStyle: TextStyle(fontSize: 12), labelText: 'End Date', hintText: ' yyyy-MM-dd', ), onTap: () async {}, ), ), ), ], ), SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Container( height: 30, width: 150, child: ElevatedButton( onPressed: () { if (formKey.currentState!.validate()) { final String docTitle = docTitleController.text; final int tokenNo = int.parse(tokenNoController.text); final String duration = durationController.text; final String startDate = startDateController.text; final String endDate = endDateController.text; setState(() { if (isEditing) { _futureDocument = updateDocument( editingDocument!.docId, docTitle, tokenNo, duration, startDate, endDate, ); } else { _futureDocument = createDocument( docTitle, tokenNo, duration, startDate, endDate, ); } }); Navigator.of(context).pushReplacement(MaterialPageRoute( builder: (context) => DocumentPage(this.userName), )); } }, child: Text(isEditing ? "Update" : "Save"), style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Color.fromARGB(255, 253, 153, 33)), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), ), ), ), ), ), Container( height: 30, width: 150, child: ElevatedButton( onPressed: () { docTitleController.clear(); tokenNoController.clear(); durationController.clear(); startDateController.clear(); endDateController.clear(); }, child: const Text("Clear"), style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Color.fromARGB(255, 253, 153, 33)), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(18.0), ), ), ), ), ), ], ), ], ), ); } FutureBuilder<Document> buildFutureBuilder() { return FutureBuilder<Document>( future: _futureDocument, builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data!.docTitle); } else if (snapshot.hasError) { return Text('${snapshot.error}'); } return const CircularProgressIndicator(); }, ); }
内容的提问来源于stack exchange,提问作者Minal Suryawanshi
相关产品推荐
相关产品推荐

