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

Flutter中输入TextFormField时页面意外关闭问题求助

Flutter对话框输入地址时意外返回首页的解决方案

问题原因分析

  1. 静态GlobalKey导致状态冲突:_formKey被声明为static,多个页面实例会共享同一个Form状态key,触发输入时Form状态异常,导致页面被意外弹出。
  2. 上下文(context)使用错误:日期/时间选择器使用的是页面级context,而非对话框内部的context,层级错误可能间接引发对话框关闭。
  3. 空值未安全处理:地址字段的validator直接强制解包value!,当输入为空时会触发空指针异常,导致页面崩溃返回首页。
  4. 外部状态干扰对话框:对话框的状态变量(如appointmentType)放在页面State中,切换状态时可能引发页面级的状态重建,导致对话框意外关闭。

解决步骤

  1. 移除GlobalKey的static修饰:让每个页面实例拥有独立的Form状态key,避免状态冲突。
  2. 传递对话框内部context:日期/时间选择器使用对话框内部的context,保证弹窗层级正确。
  3. 安全处理空值校验:修改地址字段的validator,先判断值是否为null再检查是否为空。
  4. 将对话框状态移至内部:把对话框相关的状态变量放在StatefulBuilder内部,避免外部状态干扰。

修改后的关键代码片段

1. 调整GlobalKey定义

// 移除static修饰,每个页面实例独立拥有FormKey
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

2. 重构showInformationDialog方法

Future<void> showInformationDialog(BuildContext context) async {
  return await showDialog(
      context: context,
      builder: (dialogContext) { // 重命名context区分页面与对话框上下文
        return StatefulBuilder(builder: (dialogContext, setInnerState) {
          // 将对话框状态移至内部,避免外部状态干扰
          int appointmentType = 0;
          DateTime bookDateTime = DateTime.now();
          bool isDatePicked = false;
          bool isTimePicked = false;
          bool isTypePicked = false;

          return AlertDialog(
            content: SingleChildScrollView(
              child: Form(
                  key: _formKey,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      TextFormField(
                        readOnly: true,
                        controller: bookDateController,
                        decoration: InputDecoration(
                            labelText: 'Date',
                            border: OutlineInputBorder(),
                            prefixIcon: Icon(Icons.date_range)
                        ),
                        validator: (value){
                          return isDatePicked ? null : 'Pick a date.';
                        },
                        onTap: () async {
                          // 使用对话框内部context调用日期选择器
                          final pickedDate = await pickDate(dialogContext);
                          if(pickedDate == null) return;

                          final dateTime = DateTime(
                            pickedDate.year,
                            pickedDate.month,
                            pickedDate.day,
                          );
                          setInnerState((){
                            bookDateTime = dateTime;
                            bookDateController.text = DateFormat('yyyy-MM-dd').format(dateTime);
                            isDatePicked = true;
                          });
                        },
                      ),
                      SizedBox(height: 12,),
                      TextFormField(
                        readOnly: true,
                        controller: bookTimeController,
                        decoration: InputDecoration(
                            labelText: 'Time',
                            border: OutlineInputBorder(
                                borderSide: BorderSide(
                                  color: Theme.of(dialogContext).primaryColor,
                                )
                            ),
                            prefixIcon: Icon(Icons.access_time_outlined)
                        ),
                        validator: (value){
                          return isTimePicked ? null : 'Pick a time';
                        },
                        onTap: () async {
                          if(!isDatePicked){
                            Fluttertoast.showToast(
                              msg: "Please pick a date first.",
                              backgroundColor: Colors.black45,
                            );
                            return;
                          }
                          // 使用对话框内部context调用时间选择器
                          final pickedTime = await pickTime(dialogContext);
                          if(pickedTime == null) return;

                          final dateTime = DateTime(
                              bookDateTime.year,
                              bookDateTime.month,
                              bookDateTime.day,
                              pickedTime.hour,
                              pickedTime.minute);
                          setInnerState((){
                            bookDateTime = dateTime;
                            bookTimeController.text = DateFormat('HH:mm').format(dateTime);
                            isTimePicked = true;
                          });
                        },
                      ),
                      SizedBox(height: 14,),
                      Text("Type", style: TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.w500,
                        color: Colors.black45,
                      )),
                      SizedBox(height: 8,),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: [
                          CustomRadioButton("Online", 1,appointmentType, (value){
                            setInnerState((){
                              appointmentType = value;
                              isTypePicked = true;
                            });
                          }, dialogContext),
                          CustomRadioButton("Home", 2,appointmentType, (value){
                            setInnerState((){
                              appointmentType = value;
                              isTypePicked = true;
                            });
                          }, dialogContext),
                          CustomRadioButton("Clinic", 3,appointmentType, (value){
                            setInnerState((){
                              appointmentType = value;
                              isTypePicked = true;
                            });
                          }, dialogContext),
                        ],
                      ),
                      SizedBox(height: 8,),
                      (appointmentType == 2) ? showAddressField(dialogContext) : Container(),
                    ],
                  )),
            ),
            title: Text('Book Appointment'),
            actions: <Widget>[
              Container(
                padding: EdgeInsets.symmetric(horizontal: 20),
                width: MediaQuery.of(dialogContext).size.width,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.black,
                        backgroundColor: Colors.white,
                        elevation: 2,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(32.0),
                        ),
                      ),
                      onPressed: () {
                        Navigator.of(dialogContext).pop();
                      },
                      child: Text(
                        'Cancel',
                        style: TextStyle(
                          color: Theme.of(dialogContext).primaryColor,
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    ElevatedButton(
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.black,
                        backgroundColor: Theme.of(dialogContext).primaryColor,
                        elevation: 2,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(32.0),
                        ),
                      ),
                      onPressed: () {
                        if (_formKey.currentState!.validate()) {
                          if(!isTypePicked){
                            Fluttertoast.showToast(msg: "Please choose the type");
                            return;
                          }
                          sendBookApi(bookDateTime, appointmentType);
                        }
                      },
                      child: Text(
                        'Submit',
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          );
        });
      });
}

3. 修改选择器方法接收context参数

Future<DateTime?> pickDate(BuildContext context)=> showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime.now(),
    lastDate: DateTime.now().add(Duration(days: 365))
);

Future<TimeOfDay?> pickTime(BuildContext context) => showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
);

4. 修改地址字段的空值校验

Widget showAddressField(BuildContext context) {
  return TextFormField(
    controller: addressController,
    decoration: InputDecoration(
        labelText: 'Your address',
        border: OutlineInputBorder(
            borderSide: BorderSide(
              color: Theme.of(context).primaryColor,
            )
        ),
        prefixIcon: Icon(Icons.location_on_outlined)
    ),
    validator: (value){
      // 安全处理空值
      if(value == null || value.isEmpty){
        return 'Type your address';
      }
      return null;
    },
  );
}

5. 修改CustomRadioButton方法接收context参数

Widget CustomRadioButton(String text, int index, int appType, Function(int) onChange, BuildContext context) {
  return OutlinedButton(
    onPressed: () {
      onChange(index);
    },
    style: OutlinedButton.styleFrom(
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10)
      ),
      side: BorderSide(
        color: (appType == index) ? Theme.of(context).primaryColor : Colors.black,
        width: (appType == index) ? 2 : 1.5,
      ),
    ),
    child: Padding(
      padding: const EdgeInsets.all(8.0),
      child: Text(
        text,
        style: TextStyle(
          fontSize: 14,
          fontWeight: (appType == index) ? FontWeight.bold : FontWeight.w500,
          color: (appType == index) ? Theme.of(context).primaryColor : Colors.black,
        ),
      ),
    ),
  );
}

6. 修改sendBookApi接收参数并使用正确上下文关闭对话框

void sendBookApi(DateTime bookDateTime, int appointmentType){
  apiController.post(
      url: APIs.BOOK_APPOINTMENT,
      data: {
        'doctor_id' : widget.doctor.id,
        'date_time': DateFormat('yyyy-MM-dd HH:mm').format(bookDateTime).toString(),
        'type': appointmentType,
        'address': addressController.text
      }
  ).then((value) {
    BaseResponse response = BaseResponse.fromJson(value.data);
    Fluttertoast.showToast(
      msg: response.message,
      toastLength: Toast.LENGTH_SHORT,
      fontSize: 16.0,
      backgroundColor: response.result == 'error' ? Colors.redAccent : Colors.green[400],
      gravity: ToastGravity.BOTTOM,
      timeInSecForIosWeb: 1,
    );
    Navigator.of(context).pop();
  });
}

7. 移除State类中的冗余状态变量

在_DoctorProfileState中删除以下变量:

// 已移至对话框内部,无需保留
// int appointmentType = 0;
// DateTime bookDateTime = DateTime.now();
// bool isDatePicked = false;
// bool isTimePicked = false;
// bool isTypePicked = false;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 08:32:01