Flutter: วิธีการใช้งาน Form เบื้องต้น

Form widget เป็นตัวช่วยเรื่องจัดการแบบฟอร์มที่ให้ผู้ใช้งานกรอกข้อมูลต่าง ๆ เป็น optional ไม่จำเป็นต้องใช้ก็ได้เพราะปกติพวก input สามารถเข้าถึงได้จาก Controller ได้อยู่แล้ว ตัว Form จะมี FormState ที่สามารถเข้าถึงโดยการใช้ GlobalKey และควบคุมการทำงานของตัวแบบฟอร์มได้

การสร้าง Form

constructor ของ Form มีดังนี้

const Form({
  Key? key,
  required Widget child,
  bool? canPop,
  PopInvokedWithResultCallback<Object?>? onPopInvokedWithResult,
  VoidCallback? onChanged,
  AutovalidateMode? autovalidateMode,
})

ให้เอา Form ไปครอบส่วนที่เป็นส่วนของ input โดย input ต่าง ๆ ที่เป็น FormField ดังนี้

การติดต่อกับ Form แนะนำให้ใช้การสร้าง GlobalKey แล้วแนบไปกับ Form เพื่อเรียกใช้คำสั่งใน FormState เป็นวิธีที่สะดวกและรวดเร็ว อีกวิธีหากไม่ต้องการใช้ GlobalKey และตัว widget ที่เรียก FormState เพื่อใช้งาน อยู่ใน child ของ Form สามารถใช้ผ่าน BuildContext ด้วยคำสั่ง context.findAncestorStateOfType<FormState> หรือ Form.of(context) แทนก็ได้

ตัวอย่างการสร้าง Form โดยมีโครงสร้างดังนี้

  • ตัวแบบฟอร์มจะมีช่องให้กรอก Name และ Email
  • ในการเข้าถึงข้อความใน Name และ Email จำเป็นต้องสร้าง TextEditingController() เพื่อใช้สื่อสารระหว่างแอปและตัว input
  • เมื่อเรียกคำสั่ง validate() จาก FormState ตัว framework จะไปเรียกตัว validator ใน FormField ทุกตัวให้ทำงาน
import 'dart:developer';
import 'package:flutter/material.dart';

void main() {
  // access FormState
  var formKey = GlobalKey<FormState>();

  // require for access value of TextFormField
  var textName = TextEditingController();
  var textEmail = TextEditingController();

  // validator callback for Name input
  String? validatorName(String? value) {
	return null; // value is valid
  }

  // validator callback for Email input
  String? validatorEmail(String? value) {
	return null; // value is valid
  }

  var form = Form(
      key: formKey, // you can access FormState through key
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          TextFormField(
              controller: textName, // you can access text value through controller
              decoration: InputDecoration(label: Text('Name')),
              validator: validatorName),
          TextFormField(
              controller: textEmail, // you can access text value through controller
              decoration: InputDecoration(label: Text('Email')),
              validator: validatorEmail),
          OutlinedButton(
              onPressed: () {
				// test form validate
			  }
              child: Text(".validate()")),
          OutlinedButton(
              onPressed: () {
				// validate and process you data here
              },
              child: Text("Submit")),
        ],
      ));

  runApp(MaterialApp(
    home: Scaffold(
      body: form,
    ),
  ));
}

ตัวอย่างสร้าง Form

คำสั่ง validate

เมื่อต้องการตรวจสอบว่าข้อมูลที่กรอกมา ถูกต้องหรือไม่ ให้เรียกใช้คำสั่ง .validate() ปกติจะเรียกก่อนที่จะทำการบันทึกหรือประมวลผลข้อมูล

เมื่อเรียกคำสั่งนี้ ตัว Form จะเข้าไปทำการเรียก callback ที่ถูกระบุไว้ใน FormField.validator ผู้พัฒนามีหน้าที่ต้องตรวจสอบข้อมูลว่าถูกต้องหรือไม่ ถ้าถูกต้อง✅ให้ส่งค่า null กลับมา แต่หากไม่ถูกต้อง❌ ให้ส่งข้อความที่แจ้งปัญหากลับมา เพื่อนำไปแสดงให้ผู้ใช้ทราบ จากตัวอย่าง ทำการเพิ่มคำสั่งตรวจสอบใน validatorName() และ validatorEmail()

  // validator callback for Name input
  String? validatorName(String? value) {
    if (value == null || value.isEmpty) {
      return 'Please enter Name';
    } else {
      return null;
    }
  }

  // validator callback for Email input
  String? validatorEmail(String? value) {
    if (value == null || value.isEmpty || !value.contains('@')) {
      return 'Please enter valid Email';
    } else {
      return null;
    }
  }
  
  
  // ....
  // add FormState.validate method to button
          OutlinedButton(
              onPressed: () => log('Form.validate() → ${formKey.currentState!.validate()}'),
              child: Text(".validate()")),
  // ....
  

ลองกดปุ่มเพื่อสั่ง validate จะเห็นว่าข้อความแจ้งปัญหาที่ส่งกลับมา จะมาแสดงที่ตัว input

เมื่อต้องการบันทึกข้อมูล

ในการประมวลผลที่ได้จาก input ผู้ใช้ต้องเรียกคำสั่ง FormField.validator ก่อนเสมอ แล้วตรวจสอบว่าผลที่ได้เป็น true หรือไม่ จึงนำเอาผลที่ได้จาก input ไปใช้งานต่อไป

หากเป็น TextFormField สามารถเข้าถึงข้อมูลข้อความที่กรอกได้จาก TextEditingController ที่ไปผูกติดเอาไว้ในแต่ละ widget โดยใช้คำสั่ง TextEditingController.text ตัวอย่าง ปุ่ม Submit ที่จะพิมพ์ข้อความที่กรอกออกทาง log

          OutlinedButton(
              onPressed: () {
                if (formKey.currentState!.validate()) {
                  log('------------------------------------');
                  log('Form validate() → true');
                  log('Name: ${textName.text}');
                  log('Email: ${textEmail.text}');
                  log('------------------------------------');
                } else {
                  log('Form validate() → false');
                }
              },
              child: Text("Submit")),

เมื่อกดปุ่ม Submit

ใช้คำสั่ง save เรียก onSaved เพื่อบันทึกข้อมูล

ใน Form ยังมีคำสั่ง save ที่จะทำหน้าที่ไปเรียก callback ที่ผูกกับ FormField.onSaved อีกที เหมาะกับการย้าย logic การเอาข้อมูลจาก input ไปบันทึกไว้ในที่ที่เตรียมไว้ ไปไว้ที่ตัว input แทน ถ้ามีการเพิ่ม input ใหม่ก็จะจัดการง่ายกว่า ไม่ต้องไปเพิ่มที่ปุ่ม Submit

ข้อดีของวิธีนี้คือ หากไม่มีความจำเป็นต้องใช้งาน TextEditingController ก็สามารถละเว้นไม่ต้องสร้างเพื่อเอาไปผูกกับ TextFormField ก็ได้ เนื่องจาก callback ส่งเรียกจาก onSaved จะส่งตัวแปรที่เป็นค่า String ของตัว TextFormField มาให้ด้วย

ตัวอย่าง จะเป็นการย้ายในส่วนของการพิมพ์ข้อมูล log เมื่อกดปุ่ม Submit ไปไว้ที่ตัว FormField แทน

  var form = Form(
      key: formKey, // you can access FormState through key
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          TextFormField(
              decoration: InputDecoration(label: Text('Name')),
              validator: validatorName,
              onSaved: (String? newValue) => log('Name: $newValue')),
          TextFormField(
              decoration: InputDecoration(label: Text('Email')),
              validator: validatorEmail,
              onSaved: (String? newValue) => log('Email: $newValue')),
          OutlinedButton(
              onPressed: () =>
                  log('Form.validate() → ${formKey.currentState!.validate()}'),
              child: Text(".validate()")),
          OutlinedButton(
              onPressed: () {
                if (formKey.currentState!.validate()) {
                  log('------------------------------------');
                  log('Form validate() → true');
                  formKey.currentState!.save(); // call onSaved in all TextFormField
                  log('------------------------------------');
                } else {
                  log('Form validate() → false');
                }
              },
              child: Text("Submit")),
        ],
      ));

ผลการทำงานจะเหมือนเดิม

การใช้งาน Form อื่น ๆ เพิ่มเติม

ทำการ validate ทุกครั้งที่ค่าใน FormField เปลี่ยนแปลง

หากต้องการให้ validate ทันทีที่ข้อมูลเปลี่ยนแปลง เมื่อผู้ใช้แก้ไขข้อความใน FormField ให้กำหนดค่า Form.autovalidateMode เป็น AutovalidateMode.always โดยการ validate จะทำทั้ง Form ไม่ใช่ทำเฉพาะตัวที่กำลังแก้ไขอยู่เท่านั้น

Form(
  autovalidateMode: AutovalidateMode.always
);	

คำสั่ง reset เพื่อเริ่มต้นใหม่

เมื่อเรียกคำสั่ง reset จะทำให้ FormField ทุกอันใน Form ทำการเริ่มต้นใหม่โดยใช้ค่าจาก FormField.initialValue และทำการเรียก callback ที่ผูกไว้กับ Form.onChanged

ถ้ามีการกำหนดค่า Form.autovalidateMode เป็น AutovalidateMode.always หลังจากเปลี่ยนค่าแล้วจะทำการ validate อีกครั้ง

การใช้คำสั่ง validateGranularly เพื่อเลือกตัว FormField ที่มีปัญหา

คำสั่ง .validateGranularly() จะทำหน้าที่ตรวจสอบข้อมูลของ FormField ว่าถูกต้องหรือไม่ จะต่างจาก .validate() ตรงที่ผลที่ได้จะเป็น Set ของตัว FormFieldState ที่มีปัญหา เพื่อให้คนพัฒนาแอปสามารถทำการเรียกคำสั่งกับตัว FormFieldState ที่มีปัญหาได้

ตัวอย่าง ถ้าเพิ่มปุ่ม เพื่อเรียกคำสั่ง validateGranularly เมื่อคลิกแล้ว จะพิมพ์ Set ของ input ที่มีปัญหาออกมา หากทุกตัวผ่านหมดจะส่งกลับมาเป็น Set ว่าง

          OutlinedButton(
              onPressed: () {
                var invalidFields = formKey.currentState!.validateGranularly();
                log(invalidFields.toString());
              },
              child: Text(".validateGranularly()")),

ผลที่ได้ บรรทัดแรกคือไม่กรอกอะไรเลย ส่วนบรรทัดที่สองคือกรอกถูกทั้งสองช่อง

[log] {_TextFormFieldState#5ad98, _TextFormFieldState#bb5ca}
[log] {}

คำสั่งที่น่าสนใจ

  • FormFieldState.reset() ล้างข้อความที่มีอยู่ทิ้ง เปลี่ยนเป็นค่าเริ่มต้น ตามที่ระบุไว้ใน initialValue
  • FormFieldState.value อ่านค่าข้อความปัจจุบันใน input
  • FormFieldState.widget อ้างถึงตัว FormField ที่ผูกกับ State ตัวดังกล่าว

ป้องกันการใช้คำสั่ง Pop เพื่อเปลี่ยนหน้า

หากต้องการไม่ให้เปลี่ยนหน้าด้วยคำสั่ง Pop ให้กำหนดค่า Form.canPop เป็น false

 
 
# Flutter: วิธีการใช้งาน Form เบื้องต้น !![](0000) Form widget เป็นตัวช่วยเรื่องจัดการแบบฟอร์มที่ให้ผู้ใช้งานกรอกข้อมูลต่าง ๆ เป็น optional ไม่จำเป็นต้องใช้ก็ได้เพราะปกติพวก input สามารถเข้าถึงได้จาก Controller ได้อยู่แล้ว ตัว Form จะมี FormState ที่สามารถเข้าถึงโดยการใช้ [GlobalKey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html) และควบคุมการทำงานของตัวแบบฟอร์มได้ ## การสร้าง Form constructor ของ Form มีดังนี้ ```dart const Form({ Key? key, required Widget child, bool? canPop, PopInvokedWithResultCallback? onPopInvokedWithResult, VoidCallback? onChanged, AutovalidateMode? autovalidateMode, }) ``` ให้เอา Form ไปครอบส่วนที่เป็นส่วนของ input โดย input ต่าง ๆ ที่เป็น [FormField](https://api.flutter.dev/flutter/widgets/FormField-class.html) ดังนี้ - [TextFormField](https://api.flutter.dev/flutter/material/TextFormField-class.html) แทน [TextField](https://api.flutter.dev/flutter/material/TextField-class.html) - [DropdownButtonFormField](https://api.flutter.dev/flutter/material/DropdownButtonFormField-class.html) แทน [DropdownButton](https://api.flutter.dev/flutter/material/DropdownButton-class.html) - [CupertinoTextFormFieldRow](https://api.flutter.dev/flutter/cupertino/CupertinoTextFormFieldRow-class.html) แทน [CupertinoTextField](https://api.flutter.dev/flutter/cupertino/CupertinoTextField-class.html) การติดต่อกับ Form แนะนำให้ใช้การสร้าง [GlobalKey](https://api.flutter.dev/flutter/widgets/GlobalKey-class.html) แล้วแนบไปกับ Form เพื่อเรียกใช้คำสั่งใน [FormState](https://api.flutter.dev/flutter/widgets/FormState-class.html) เป็นวิธีที่สะดวกและรวดเร็ว อีกวิธีหากไม่ต้องการใช้ GlobalKey และตัว widget ที่เรียก FormState เพื่อใช้งาน อยู่ใน child ของ Form สามารถใช้ผ่าน [BuildContext](20241117_fultter_buildcontext_class.html) ด้วยคำสั่ง `context.findAncestorStateOfType` หรือ `Form.of(context)` แทนก็ได้ ตัวอย่างการสร้าง Form โดยมีโครงสร้างดังนี้ - ตัวแบบฟอร์มจะมีช่องให้กรอก Name และ Email - ในการเข้าถึงข้อความใน Name และ Email จำเป็นต้องสร้าง TextEditingController() เพื่อใช้สื่อสารระหว่างแอปและตัว input - เมื่อเรียกคำสั่ง `validate()` จาก FormState ตัว framework จะไปเรียกตัว validator ใน FormField ทุกตัวให้ทำงาน ```dart import 'dart:developer'; import 'package:flutter/material.dart'; void main() { // access FormState var formKey = GlobalKey(); // require for access value of TextFormField var textName = TextEditingController(); var textEmail = TextEditingController(); // validator callback for Name input String? validatorName(String? value) { return null; // value is valid } // validator callback for Email input String? validatorEmail(String? value) { return null; // value is valid } var form = Form( key: formKey, // you can access FormState through key child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextFormField( controller: textName, // you can access text value through controller decoration: InputDecoration(label: Text('Name')), validator: validatorName), TextFormField( controller: textEmail, // you can access text value through controller decoration: InputDecoration(label: Text('Email')), validator: validatorEmail), OutlinedButton( onPressed: () { // test form validate } child: Text(".validate()")), OutlinedButton( onPressed: () { // validate and process you data here }, child: Text("Submit")), ], )); runApp(MaterialApp( home: Scaffold( body: form, ), )); } ``` !![ ตัวอย่างสร้าง Form ](0100) ## คำสั่ง validate เมื่อต้องการตรวจสอบว่าข้อมูลที่กรอกมา ถูกต้องหรือไม่ ให้เรียกใช้คำสั่ง [`.validate()`](https://api.flutter.dev/flutter/widgets/FormState/validate.html) ปกติจะเรียกก่อนที่จะทำการบันทึกหรือประมวลผลข้อมูล เมื่อเรียกคำสั่งนี้ ตัว Form จะเข้าไปทำการเรียก callback ที่ถูกระบุไว้ใน [FormField.validator](https://api.flutter.dev/flutter/widgets/FormField/validator.html) ผู้พัฒนามีหน้าที่ต้องตรวจสอบข้อมูลว่าถูกต้องหรือไม่ ถ้าถูกต้อง✅ให้ส่งค่า `null` กลับมา แต่หากไม่ถูกต้อง❌ ให้ส่ง**ข้อความ**ที่แจ้งปัญหากลับมา เพื่อนำไปแสดงให้ผู้ใช้ทราบ จากตัวอย่าง ทำการเพิ่มคำสั่งตรวจสอบใน `validatorName()` และ `validatorEmail()` ```dart // validator callback for Name input String? validatorName(String? value) { if (value == null || value.isEmpty) { return 'Please enter Name'; } else { return null; } } // validator callback for Email input String? validatorEmail(String? value) { if (value == null || value.isEmpty || !value.contains('@')) { return 'Please enter valid Email'; } else { return null; } } // .... // add FormState.validate method to button OutlinedButton( onPressed: () => log('Form.validate() → ${formKey.currentState!.validate()}'), child: Text(".validate()")), // .... ``` !![ ลองกดปุ่มเพื่อสั่ง validate จะเห็นว่าข้อความแจ้งปัญหาที่ส่งกลับมา จะมาแสดงที่ตัว input ](0200) ## เมื่อต้องการบันทึกข้อมูล ในการประมวลผลที่ได้จาก input ผู้ใช้ต้องเรียกคำสั่ง [FormField.validator](https://api.flutter.dev/flutter/widgets/FormField/validator.html) ก่อนเสมอ แล้วตรวจสอบว่าผลที่ได้เป็น `true` หรือไม่ จึงนำเอาผลที่ได้จาก input ไปใช้งานต่อไป หากเป็น TextFormField สามารถเข้าถึงข้อมูลข้อความที่กรอกได้จาก TextEditingController ที่ไปผูกติดเอาไว้ในแต่ละ widget โดยใช้คำสั่ง [TextEditingController.text](https://api.flutter.dev/flutter/widgets/TextEditingController/text.html) ตัวอย่าง ปุ่ม Submit ที่จะพิมพ์ข้อความที่กรอกออกทาง log ```dart OutlinedButton( onPressed: () { if (formKey.currentState!.validate()) { log('------------------------------------'); log('Form validate() → true'); log('Name: ${textName.text}'); log('Email: ${textEmail.text}'); log('------------------------------------'); } else { log('Form validate() → false'); } }, child: Text("Submit")), ``` !![ เมื่อกดปุ่ม Submit ](0300) ## ใช้คำสั่ง save เรียก onSaved เพื่อบันทึกข้อมูล ใน Form ยังมีคำสั่ง [save](https://api.flutter.dev/flutter/widgets/FormState/save.html) ที่จะทำหน้าที่ไปเรียก callback ที่ผูกกับ [FormField.onSaved](https://api.flutter.dev/flutter/widgets/FormField/onSaved.html) อีกที เหมาะกับการย้าย logic การเอาข้อมูลจาก input ไปบันทึกไว้ในที่ที่เตรียมไว้ ไปไว้ที่ตัว input แทน ถ้ามีการเพิ่ม input ใหม่ก็จะจัดการง่ายกว่า ไม่ต้องไปเพิ่มที่ปุ่ม Submit ข้อดีของวิธีนี้คือ หากไม่มีความจำเป็นต้องใช้งาน TextEditingController ก็สามารถละเว้นไม่ต้องสร้างเพื่อเอาไปผูกกับ TextFormField ก็ได้ เนื่องจาก callback ส่งเรียกจาก onSaved จะส่งตัวแปรที่เป็นค่า String ของตัว TextFormField มาให้ด้วย ตัวอย่าง จะเป็นการย้ายในส่วนของการพิมพ์ข้อมูล log เมื่อกดปุ่ม Submit ไปไว้ที่ตัว FormField แทน ```dart var form = Form( key: formKey, // you can access FormState through key child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ TextFormField( decoration: InputDecoration(label: Text('Name')), validator: validatorName, onSaved: (String? newValue) => log('Name: $newValue')), TextFormField( decoration: InputDecoration(label: Text('Email')), validator: validatorEmail, onSaved: (String? newValue) => log('Email: $newValue')), OutlinedButton( onPressed: () => log('Form.validate() → ${formKey.currentState!.validate()}'), child: Text(".validate()")), OutlinedButton( onPressed: () { if (formKey.currentState!.validate()) { log('------------------------------------'); log('Form validate() → true'); formKey.currentState!.save(); // call onSaved in all TextFormField log('------------------------------------'); } else { log('Form validate() → false'); } }, child: Text("Submit")), ], )); ``` !![ ผลการทำงานจะเหมือนเดิม ](0400) ## การใช้งาน Form อื่น ๆ เพิ่มเติม ### ทำการ validate ทุกครั้งที่ค่าใน FormField เปลี่ยนแปลง หากต้องการให้ validate ทันทีที่ข้อมูลเปลี่ยนแปลง เมื่อผู้ใช้แก้ไขข้อความใน FormField ให้กำหนดค่า [Form.autovalidateMode](https://api.flutter.dev/flutter/widgets/Form/autovalidateMode.html) เป็น [AutovalidateMode.always](https://api.flutter.dev/flutter/widgets/AutovalidateMode.html#always) โดยการ validate จะทำทั้ง Form ไม่ใช่ทำเฉพาะตัวที่กำลังแก้ไขอยู่เท่านั้น ```dart Form( autovalidateMode: AutovalidateMode.always ); ``` ### คำสั่ง reset เพื่อเริ่มต้นใหม่ เมื่อเรียกคำสั่ง reset จะทำให้ FormField ทุกอันใน Form ทำการเริ่มต้นใหม่โดยใช้ค่าจาก [FormField.initialValue](https://api.flutter.dev/flutter/widgets/FormField/initialValue.html) และทำการเรียก callback ที่ผูกไว้กับ [Form.onChanged](https://api.flutter.dev/flutter/widgets/Form/onChanged.html) ถ้ามีการกำหนดค่า [Form.autovalidateMode](https://api.flutter.dev/flutter/widgets/Form/autovalidateMode.html) เป็น [AutovalidateMode.always](https://api.flutter.dev/flutter/widgets/AutovalidateMode.html#always) หลังจากเปลี่ยนค่าแล้วจะทำการ validate อีกครั้ง ### การใช้คำสั่ง validateGranularly เพื่อเลือกตัว FormField ที่มีปัญหา คำสั่ง [.validateGranularly()](https://api.flutter.dev/flutter/widgets/FormState/validateGranularly.html) จะทำหน้าที่ตรวจสอบข้อมูลของ FormField ว่าถูกต้องหรือไม่ จะต่างจาก [`.validate()`](https://api.flutter.dev/flutter/widgets/FormState/validate.html) ตรงที่ผลที่ได้จะเป็น [Set](20240815_dart_set.html) ของตัว [FormFieldState](https://api.flutter.dev/flutter/widgets/FormFieldState-class.html) ที่มีปัญหา เพื่อให้คนพัฒนาแอปสามารถทำการเรียกคำสั่งกับตัว FormFieldState ที่มีปัญหาได้ ตัวอย่าง ถ้าเพิ่มปุ่ม เพื่อเรียกคำสั่ง validateGranularly เมื่อคลิกแล้ว จะพิมพ์ Set ของ input ที่มีปัญหาออกมา หากทุกตัวผ่านหมดจะส่งกลับมาเป็น [Set](20240815_dart_set.html) ว่าง ```dart OutlinedButton( onPressed: () { var invalidFields = formKey.currentState!.validateGranularly(); log(invalidFields.toString()); }, child: Text(".validateGranularly()")), ``` ผลที่ได้ บรรทัดแรกคือไม่กรอกอะไรเลย ส่วนบรรทัดที่สองคือกรอกถูกทั้งสองช่อง > [log] {_TextFormFieldState#5ad98, _TextFormFieldState#bb5ca}
> [log] {}
คำสั่งที่น่าสนใจ - [FormFieldState.reset()](https://api.flutter.dev/flutter/widgets/FormFieldState/reset.html) ล้างข้อความที่มีอยู่ทิ้ง เปลี่ยนเป็นค่าเริ่มต้น ตามที่ระบุไว้ใน initialValue - [FormFieldState.value](https://api.flutter.dev/flutter/widgets/FormFieldState/value.html) อ่านค่าข้อความปัจจุบันใน input - [FormFieldState.widget](https://api.flutter.dev/flutter/widgets/State/widget.html) อ้างถึงตัว [FormField](https://api.flutter.dev/flutter/widgets/FormField-class.html) ที่ผูกกับ State ตัวดังกล่าว ### ป้องกันการใช้คำสั่ง Pop เพื่อเปลี่ยนหน้า หากต้องการไม่ให้เปลี่ยนหน้าด้วยคำสั่ง Pop ให้กำหนดค่า [Form.canPop](https://api.flutter.dev/flutter/widgets/Form/canPop.html) เป็น `false`