Form widget เป็นตัวช่วยเรื่องจัดการแบบฟอร์มที่ให้ผู้ใช้งานกรอกข้อมูลต่าง ๆ เป็น optional ไม่จำเป็นต้องใช้ก็ได้เพราะปกติพวก input สามารถเข้าถึงได้จาก Controller ได้อยู่แล้ว ตัว Form จะมี FormState ที่สามารถเข้าถึงโดยการใช้ GlobalKey และควบคุมการทำงานของตัวแบบฟอร์มได้
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 โดยมีโครงสร้างดังนี้
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()
ปกติจะเรียกก่อนที่จะทำการบันทึกหรือประมวลผลข้อมูล
เมื่อเรียกคำสั่งนี้ ตัว 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
ใน 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")),
],
));
ผลการทำงานจะเหมือนเดิม
หากต้องการให้ validate ทันทีที่ข้อมูลเปลี่ยนแปลง เมื่อผู้ใช้แก้ไขข้อความใน FormField ให้กำหนดค่า Form.autovalidateMode เป็น AutovalidateMode.always โดยการ validate จะทำทั้ง Form ไม่ใช่ทำเฉพาะตัวที่กำลังแก้ไขอยู่เท่านั้น
Form(
autovalidateMode: AutovalidateMode.always
);
เมื่อเรียกคำสั่ง reset จะทำให้ FormField ทุกอันใน Form ทำการเริ่มต้นใหม่โดยใช้ค่าจาก FormField.initialValue และทำการเรียก callback ที่ผูกไว้กับ Form.onChanged
ถ้ามีการกำหนดค่า Form.autovalidateMode เป็น AutovalidateMode.always หลังจากเปลี่ยนค่าแล้วจะทำการ validate อีกครั้ง
คำสั่ง .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] {}
คำสั่งที่น่าสนใจ
หากต้องการไม่ให้เปลี่ยนหน้าด้วยคำสั่ง Pop ให้กำหนดค่า Form.canPop เป็น false