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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 11:45:23